<?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>How to Resize an Image by Exact Pixels or by Percentage via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:09:52 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-resize-an-image-by-exact-pixels-or-by-percentage-via-api-32gn</link>
      <guid>https://dev.to/pdf4me/how-to-resize-an-image-by-exact-pixels-or-by-percentage-via-api-32gn</guid>
      <description>&lt;p&gt;Ask five developers what "resize an image" means and you will get five different answers. One means shrinking a 4000px camera photo down to a 200px thumbnail. Another means scaling every image in a batch to 80% so a page loads faster. A third means forcing every upload into an exact 600x600 box regardless of what came in. These are not the same operation, and the way most image libraries handle them (one function, a dozen optional flags, an aspect ratio bug waiting to happen) is exactly why resize logic tends to accumulate as one of those "someone wrote this three years ago and nobody wants to touch it" corners of a codebase.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/resize-image/" rel="noopener noreferrer"&gt;Resize Image&lt;/a&gt; endpoint treats the two real-world resize methods as two real, distinct parameters instead of one overloaded function, and that distinction is worth understanding before you wire it into anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Percentage and pixels are different problems, not two flags on the same one
&lt;/h2&gt;

&lt;p&gt;Resize by percentage answers "make this smaller, proportionally, without me having to know its exact dimensions." A batch of product photos that arrive at wildly different resolutions can all get scaled down by 50% and come out proportionally consistent, whatever their starting size.&lt;/p&gt;

&lt;p&gt;Resize by exact pixel dimensions answers a completely different question: "I need this to be 600 by 400, full stop." That is the shape of the problem when a destination has a hard requirement: a thumbnail grid that expects uniform tiles, a CMS field with a fixed image slot, a print template with a defined canvas. Asking a single "resize" call to guess which one you meant is how you end up with distorted product photos or under-sized thumbnails nobody caught in code review.&lt;/p&gt;

&lt;p&gt;PDF4me's endpoint exposes both as first-class options via the &lt;code&gt;ImageResizeType&lt;/code&gt; field, which takes either &lt;code&gt;Percentage&lt;/code&gt; or &lt;code&gt;Specific&lt;/code&gt;. You pick the method that matches the actual problem instead of coercing one flag to do both jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the request actually looks like
&lt;/h2&gt;

&lt;p&gt;The REST call is a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/api/v2/ResizeImage&lt;/code&gt;, and the request body is unremarkable in the way a well-designed document API's request body should be. Here is the field-by-field shape, verified against the official &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/resize-image/" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt; Python sample rather than just the marketing-page example:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resize_image&lt;/span&gt;&lt;span class="p"&gt;():&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;get the API key from https://dev.pdf4me.com/dashboard/#/api-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;image_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;sample.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;output_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;Resize_image_output.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;base_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&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v2/ResizeImage&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;image_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;image_content&lt;/span&gt; &lt;span class="o"&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="n"&gt;image_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;image_content&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="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;image_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;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;image_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;ImageResizeType&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;Percentage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# or "Specific"
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResizePercentage&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.0&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;Width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                          &lt;span class="c1"&gt;# used when ImageResizeType is "Specific"
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;# used when ImageResizeType is "Specific"
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MaintainAspectRatio&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;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="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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&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 completion
&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;output_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;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;# Asynchronous processing: poll the Location header until done
&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;attempt&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="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;output_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;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_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;out_file&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;poll&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;break&lt;/span&gt;
            &lt;span class="k"&gt;elif&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;202&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;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="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;poll&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;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out that are easy to miss from the docs page alone: &lt;code&gt;ImageResizeType&lt;/code&gt; takes exactly two values, &lt;code&gt;Percentage&lt;/code&gt; or &lt;code&gt;Specific&lt;/code&gt;, and both &lt;code&gt;Width&lt;/code&gt;/&lt;code&gt;Height&lt;/code&gt; and &lt;code&gt;ResizePercentage&lt;/code&gt; are sent in every request regardless of which mode you use, the endpoint just ignores whichever pair doesn't apply to the selected type. The &lt;code&gt;isAsync&lt;/code&gt; flag is real and documented in the official sample even though the docs page's own JSON example doesn't show it: set it to &lt;code&gt;true&lt;/code&gt; and a large or slow-processing image returns a &lt;code&gt;202&lt;/code&gt; with a &lt;code&gt;Location&lt;/code&gt; header to poll instead of holding the connection open, the same async pattern used across PDF4me's other endpoints. Authentication uses &lt;code&gt;Authorization: Basic {api_key}&lt;/code&gt;, and the response for a synchronous &lt;code&gt;200&lt;/code&gt; is the resized image itself as binary content, not a JSON wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aspect ratio setting nobody reads until something looks wrong
&lt;/h2&gt;

&lt;p&gt;Here is the setting that actually decides whether your resized image looks correct or looks broken: &lt;code&gt;MaintainAspectRatio&lt;/code&gt;. Leave it &lt;code&gt;true&lt;/code&gt; and a percentage or pixel resize scales width and height together, so a landscape photo stays a landscape photo, just smaller. Set it &lt;code&gt;false&lt;/code&gt;, or set a target width and height that do not match the source's proportions, and you get exactly what you asked for: an image forced into a box, stretched or squashed to fit.&lt;/p&gt;

&lt;p&gt;There is a real use case for both. A thumbnail grid that needs every tile to be a literal square wants &lt;code&gt;MaintainAspectRatio: false&lt;/code&gt; and a fixed pixel target, because uniformity is the point. A hero image being scaled down for a slower connection wants it locked &lt;code&gt;true&lt;/code&gt;, because nobody wants a portrait photo of a person rendered as a slightly wider person. The mistake is not knowing which one your integration is set to, and finding out only when a customer screenshots a warped logo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resizing without touching the REST payload yourself
&lt;/h2&gt;

&lt;p&gt;Not every team wants to own a resize function, and PDF4me's no-code integrations exist for exactly that reason. In &lt;a href="https://docs.pdf4me.com/integration/power-automate/image/resize-image/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, the Resize Image action takes the same percentage-or-dimensions choice and the same aspect ratio control, dropped into a flow alongside whatever triggers it: a new file landing in SharePoint, an email attachment, a form submission, with batch processing built into the action itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/image/resize-image/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; ships the same capability under the name "Smart Scaler," and if you are already routing images through a Zap, inserting resize as a middle step means you never write image-processing code at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/image/resize-image/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;'s node is aimed squarely at the two jobs developers actually reach for it for: generating thumbnails on the fly and normalizing a pile of inconsistent uploads into one predictable size, both by percentage or exact pixels, with aspect ratio preserved by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/image/resize-image/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; covers percentage-based scaling for jobs like thumbnail generation and web optimization, with one caveat worth knowing before you build around it: there is no dedicated batch-resize mode in the module itself. Resizing more than one file means wrapping the module in a Make Iterator and letting it run the same percentage setting once per image. If your scenario already resizes a folder of files one at a time, this is not a limitation you will notice. If you are picturing a single-step bulk operation, plan for the Iterator up front rather than after your first test run comes up short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the exact request before you build a pipeline around it
&lt;/h2&gt;

&lt;p&gt;Before wiring resize into a workflow that will run unattended, confirm the exact request and response shape against a real image using PDF4me's feature-specific &lt;a href="https://docs.pdf4me.com/url-api-tester/resize-image/" rel="noopener noreferrer"&gt;Resize Image API Tester&lt;/a&gt; (or the general &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; for any other endpoint), which sends live requests from the browser and returns the actual response, no code required. This matters more for resize than it might for a simpler endpoint: aspect ratio behavior, the decimal format for percentage values, and how a non-square pixel target actually renders are all things that are faster to see once, live, than to debug after the fact in a batch job.&lt;/p&gt;

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

&lt;p&gt;The pattern that comes up most is normalization: user uploads arrive in every resolution imaginable, and a fixed downstream requirement (a thumbnail grid, a storage budget, a CMS image slot) needs them all brought to one size before anything else touches them. The second most common pattern is bandwidth: serving a smaller percentage-scaled version of a large photo to a page that does not need full resolution. A third, less obvious pattern is compliance with a third-party spec: marketplaces, print vendors, and ad networks routinely publish exact pixel requirements for submitted images, and rejecting a file for being the wrong size is a worse customer experience than resizing it automatically before it ever gets uploaded.&lt;/p&gt;

&lt;p&gt;None of these needs a dedicated image-processing service, a native image library your team now has to patch for security updates, or a homegrown wrapper around one. It is one endpoint, or one no-code action, doing one job correctly, and because it sits on the same PDF4me surface as the rest of the image and document toolset, a resize step chains naturally into a larger pipeline without switching services or re-authenticating partway through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you have not connected to the PDF4me API before, 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, API keys, and response codes, and is the fastest path to your first successful call, resize or otherwise.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>What Is Base64 Encoding, and Why Does Every PDF API Call Depend On It?</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 21 Sep 2026 11:31:56 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-is-base64-encoding-and-why-does-every-pdf-api-call-depend-on-it-4a8o</link>
      <guid>https://dev.to/pdf4me/what-is-base64-encoding-and-why-does-every-pdf-api-call-depend-on-it-4a8o</guid>
      <description>&lt;p&gt;Open a PDF4me API payload and you'll usually find one field that looks like garbage: &lt;code&gt;docContent&lt;/code&gt;, a wall of letters, numbers, plus signs, and slashes that goes on for thousands of characters. That field is doing more work than almost anything else in the request, and it has nothing to do with security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Base64 is a translator, not a lock
&lt;/h2&gt;

&lt;p&gt;Base64 encoding takes binary data (the raw bytes of a PDF, an image, anything) and re-represents it using only 64 printable ASCII characters: A-Z, a-z, 0-9, plus &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;. It is fully reversible, no key required. Anyone can decode it back to the original bytes in one line of code. If your PDF's content matters for compliance or confidentiality, encryption is a separate concern; base64 is a format conversion, not a lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it exists at all: JSON and HTTP headers are text-only
&lt;/h2&gt;

&lt;p&gt;HTTP requests and the JSON bodies inside them are text protocols. A raw PDF or image file is binary: arbitrary byte values, including bytes that would break JSON's own syntax if pasted in directly. Base64 sidesteps that entirely by re-encoding the file as plain text first, so it can sit safely inside a JSON string, get logged, get copy-pasted into Postman, and travel through any text-based transport without corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually shows up in a PDF4me call
&lt;/h2&gt;

&lt;p&gt;Take PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/image-extract-text/" rel="noopener noreferrer"&gt;Image Extract Text&lt;/a&gt; OCR endpoint. The request body is two 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;"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;"invoice-scan.jpg"&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;"iVBORw0KGgoAAAANSUhEUgAAA...(thousands more characters)"&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;docName&lt;/code&gt; is just the filename. &lt;code&gt;docContent&lt;/code&gt; is documented plainly as "the complete content of the source image encoded in Base64 format" -- that's the entire image, every byte of it, sitting inside a JSON string field. This same pattern (&lt;code&gt;docContent&lt;/code&gt; carrying a base64 blob, &lt;code&gt;docName&lt;/code&gt; carrying the filename) repeats across PDF4me's REST API for the actions that take a file in and hand one back.&lt;/p&gt;

&lt;p&gt;Worth clearing up a common assumption while we're here: your API key itself isn't base64-wrapped. Per PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;REST integration guide&lt;/a&gt;, the key goes in the &lt;code&gt;Authorization&lt;/code&gt; header as-is. Base64 in a PDF4me call is specifically about getting binary file bytes through a text-only pipe, not about how your credentials travel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The size cost nobody mentions upfront
&lt;/h2&gt;

&lt;p&gt;Base64 isn't compression, it's the opposite. Encoding binary data this way inflates the payload by roughly 33%: every 3 raw bytes become 4 encoded characters. A 3 MB PDF becomes roughly a 4 MB &lt;code&gt;docContent&lt;/code&gt; string. For a handful of documents that's nothing. For a batch job pushing hundreds of large PDFs through an endpoint in a loop, that 33% is real bandwidth, real memory footprint, and, on a slow connection, real wall-clock time before the request even reaches PDF4me's servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs that don't look like encoding bugs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The stray prefix.&lt;/strong&gt; If you grab a base64 string from a browser's &lt;code&gt;FileReader.readAsDataURL()&lt;/code&gt; (very common in JavaScript) it comes back as &lt;code&gt;data:image/png;base64,iVBORw0KGgo...&lt;/code&gt;, a data URI, not a raw base64 string. Drop that whole thing into &lt;code&gt;docContent&lt;/code&gt; unmodified and the request fails with what looks like a malformed-file error. The fix is a one-line string split on the comma; the failure mode just doesn't announce itself as an encoding problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent truncation.&lt;/strong&gt; Logging middleware, some HTTP clients, and more than a few "just print the payload for debugging" habits will truncate a long string without warning. A &lt;code&gt;docContent&lt;/code&gt; value cut off mid-stream produces a technically valid-looking but corrupted base64 string, and PDF4me will (correctly) reject it as unreadable file content. If a request fails intermittently on larger files specifically, checking whether something in the request pipeline has a hidden string-length limit is worth doing before assuming the API itself is at fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where no-code platforms make this disappear entirely
&lt;/h2&gt;

&lt;p&gt;Building this same OCR call in &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-power-automate/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-zapier/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-make/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, or &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-n8n/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; instead of raw REST, you generally hand the connector a file, not a base64 string. The encoding step still happens, it's just handled internally by the connector before the request reaches PDF4me. That's a reasonable reason to reach for a no-code workflow over a hand-rolled script: the base64 conversion is exactly the kind of plumbing you don't want to own by hand across dozens of workflow steps.&lt;/p&gt;

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

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;Interactive API Tester&lt;/a&gt; lets you upload a file and fire a real request straight from the browser, no code required, which is a fast way to see an actual request/response pair before you wire anything into your own application.&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>tutorial</category>
      <category>webdev</category>
      <category>restapi</category>
    </item>
    <item>
      <title>API Key or OAuth? Why a Document API Doesn't Need Both</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Sun, 20 Sep 2026 05:02:08 +0000</pubDate>
      <link>https://dev.to/pdf4me/api-key-or-oauth-why-a-document-api-doesnt-need-both-1e3n</link>
      <guid>https://dev.to/pdf4me/api-key-or-oauth-why-a-document-api-doesnt-need-both-1e3n</guid>
      <description>&lt;p&gt;Every new integration kicks off with the same fork in the road: does this API want an API key, or does it want OAuth? Most developers pick based on habit rather than the actual shape of the problem, and for a document processing API, that habit is usually wrong in an interesting way. The honest answer for something like PDF4me is that you don't choose between the two. You use an API key, and you stop there, because OAuth is solving a problem this kind of API doesn't have.&lt;/p&gt;

&lt;p&gt;That sounds like a shortcut. It isn't. It's a description of what OAuth actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OAuth is actually for
&lt;/h2&gt;

&lt;p&gt;OAuth 2.0 exists to answer one specific question: can this third-party application act on behalf of a specific human user, with that user's explicit, revocable consent? That's why the flow looks the way it does. A user gets redirected to a login screen they recognize, they see a consent page listing exactly what the app wants ("read your calendar," "post on your behalf"), and the app walks away with a short-lived access token plus a refresh token so it can keep working without asking the user to log in again every hour.&lt;/p&gt;

&lt;p&gt;Every piece of that machinery exists to protect the user from the app, and to let the user cut the app off later without changing a password. It's the right tool when Slack wants read access to your Google Drive, or when a scheduling tool wants to create events on your behalf. There's a human in the middle whose consent matters, and whose access needs to be revocable independent of anyone else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a document API actually is
&lt;/h2&gt;

&lt;p&gt;Now look at what happens when your backend calls &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;PDF4me's REST API&lt;/a&gt; to convert a file, merge two PDFs, or pull structured data out of an invoice. There's no end user in that request. Your server is talking to PDF4me's server. Nobody is being asked to log in, because nobody needs to consent to anything, because the only party whose authorization is being checked is your own application.&lt;/p&gt;

&lt;p&gt;This is a machine-to-machine call, and machine-to-machine calls have a much simpler question to answer: is this specific request coming from someone we've already agreed to trust? That's exactly what an API key answers. &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Generate one from the dashboard&lt;/a&gt;, attach it to the request, and PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;V2 API&lt;/a&gt; knows who's calling and whether they're allowed to. No redirect, no consent screen, no refresh cycle. One credential, one header, done.&lt;/p&gt;

&lt;p&gt;Here's exactly what that header looks like in practice, verified against PDF4me's own &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;official Python samples&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;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;get the API key from https://dev.pdf4me.com/dashboard/#/api-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;base_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/&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;api/v2/GetPdfMetadata&lt;/span&gt;&lt;span class="sh"&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;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;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="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="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="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;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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth calling out because it trips people up: that &lt;code&gt;Authorization: Basic {api_key}&lt;/code&gt; header is not classic HTTP Basic auth (a base64-encoded &lt;code&gt;username:password&lt;/code&gt; pair). It's PDF4me's own convention of the literal word &lt;code&gt;Basic&lt;/code&gt; followed by the raw API key. Copy the header name from a generic HTTP client tutorial and you'll reach for &lt;code&gt;Bearer&lt;/code&gt; out of habit; the samples across Python, C#, Java, and Apex are consistent on &lt;code&gt;Basic&lt;/code&gt;, so that's what to type. (Two older Google Apps Script samples in the same repo still use &lt;code&gt;Bearer&lt;/code&gt; instead, which is worth a second look if you're adapting from one of those specifically.)&lt;/p&gt;

&lt;p&gt;Building a full OAuth client just to make that same call would mean standing up a token exchange flow, handling refresh tokens before they expire, and maintaining a client registration you never actually needed, all to protect a human who was never in the flow to begin with. That isn't more secure. It's more surface area for the exact same outcome.&lt;/p&gt;

&lt;p&gt;Picture the actual code path. A background job picks up an uploaded contract, needs it converted to PDF/A before it lands in an archive, and calls the conversion endpoint. There's no browser tab open. There's no session. There's no user sitting there to approve a consent screen, because the "user" of this request is a cron job that runs at 2 a.m. Ask what an OAuth authorization code flow would even redirect to in that scenario, and the answer is nothing, because there's no interactive surface for it to redirect through. You'd end up faking the human step just to satisfy a protocol that assumes one exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the complexity actually goes
&lt;/h2&gt;

&lt;p&gt;Ask yourself what OAuth would even protect here. There's no consent to revoke on behalf of an end user, because there's no end user. There's no third-party app you're authorizing to act "on your behalf," because your own backend is the caller. The only thing left to protect is the credential itself, and API keys protect that the same way OAuth's client secrets do: keep it out of source control, keep it out of client-side code, and rotate it when you suspect it's been exposed.&lt;/p&gt;

&lt;p&gt;That's not a shortcut version of security. It's the actual security model, sized correctly for what's being secured. A stolen API key and a stolen OAuth refresh token cause the same damage: an attacker can now call the API as you. Neither scheme protects you from a leaked secret. What OAuth adds on top, scoped, revocable, per-user delegation, is real value when there's a user to delegate for. When there isn't, it's just more moving parts standing between your code and the request you wanted to make.&lt;/p&gt;

&lt;p&gt;There's also a cost people rarely price in: every extra moving part is one more thing that can break at 2 a.m. and page someone. A refresh token that silently expires, a client secret that rotates on the provider's schedule instead of yours, a redirect URI that stops matching after a domain migration, all of these are OAuth-shaped incidents that a single long-lived API key simply doesn't create. Fewer moving parts means fewer 2 a.m. incidents, and that's worth something on its own, independent of which approach is theoretically more secure on paper.&lt;/p&gt;

&lt;p&gt;None of this means an API key should be treated casually. Where does it live in your app? Ideally a secrets manager or environment variable your CI pipeline injects at deploy time, never a value typed into a config file that gets committed alongside everything else. How is it scoped? One key per environment, so a compromised staging key doesn't hand an attacker production access too. How is it monitored? Watch for a sudden spike in calls from a key that normally sits quiet overnight, because that pattern is often the first visible sign something leaked. None of that is unique to API keys, either. It's the same operational discipline any credential needs, OAuth client secrets included.&lt;/p&gt;

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

&lt;p&gt;The no-code side of PDF4me makes the point even more clearly, because it strips away anything a developer might rationalize as "extra flexibility." When you &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-power-automate/" rel="noopener noreferrer"&gt;connect PDF4me to Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-zapier/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-make/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, or &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-n8n/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, the entire connection step is: paste your API key. That's it. Power Automate's &lt;a href="https://docs.pdf4me.com/integration/power-automate/authorization/" rel="noopener noreferrer"&gt;authorization documentation&lt;/a&gt; walks through exactly what the connector needs and what it can do with it, and the &lt;a href="https://docs.pdf4me.com/integration/power-automate/getting-started/" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; has you authenticated and running a flow in the same session. &lt;a href="https://docs.pdf4me.com/integration/make/getting-started/" rel="noopener noreferrer"&gt;Make's own setup&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/getting-started/" rel="noopener noreferrer"&gt;n8n's&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/zapier/getting-started/" rel="noopener noreferrer"&gt;Zapier's&lt;/a&gt; all follow the identical shape: one key, one field, no separate identity provider to configure.&lt;/p&gt;

&lt;p&gt;Compare that to what an OAuth-based connector setup usually looks like: register an app with a provider, configure redirect URLs, click through a consent screen, and hope the refresh token doesn't silently expire six months later and break a workflow nobody's watching. None of that complexity buys the no-code user anything, because they aren't delegating access on behalf of someone else either. They're the same person who owns the PDF4me account and the automation that's calling it.&lt;/p&gt;

&lt;p&gt;If you want to see this before wiring anything into your own code, the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; lets you paste in that same API key and fire real requests at real endpoints directly in the browser, with no client library and no auth flow beyond the one credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you'd actually want OAuth
&lt;/h2&gt;

&lt;p&gt;None of this means OAuth is wrong, only that it's answering a different question than "how do I authenticate my backend against a document API." If you're building a multi-tenant product where your customers each connect their own separate PDF4me account, and you need per-customer consent, audit trails, and the ability for a single customer to revoke your app's access without affecting anyone else's, that's a genuine delegation problem, and OAuth is the right shape for it. Say you're building a document-automation SaaS product where each of your customers has their own PDF4me account, and your app processes files on their behalf, one connection per customer, each revocable independently, each showing up in that customer's own usage and billing. That's real delegation, with a real third party in the middle, and it's exactly the case OAuth was built to handle. It's a different product decision than "I need to convert a file," and it's worth naming honestly instead of reaching for OAuth by default because it sounds more enterprise.&lt;/p&gt;

&lt;p&gt;For the actual document processing call, the honest advice is smaller and less impressive: get your key from the &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;, never commit it to a public repository, rotate it if you think it leaked, and stop there. The security work that matters is boring key hygiene, not which auth protocol looks more sophisticated on a whiteboard.&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>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Convert a PDF Page to PNG or JPEG: Rendering Images from a PDF via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:21:33 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-convert-a-pdf-page-to-png-or-jpeg-rendering-images-from-a-pdf-via-api-38ba</link>
      <guid>https://dev.to/pdf4me/how-to-convert-a-pdf-page-to-png-or-jpeg-rendering-images-from-a-pdf-via-api-38ba</guid>
      <description>&lt;p&gt;Somewhere in your product, a PDF needs to stop being a PDF for a second. Maybe it's a thumbnail grid in a document management screen. Maybe it's a preview image you drop into a Slack message so a reviewer doesn't have to open a viewer at all. Maybe it's a first-page snapshot you show in a catalog, or a screenshot you archive as visual proof that an invoice rendered the way you expected before it went out the door.&lt;/p&gt;

&lt;p&gt;In every one of those cases, the answer isn't "open the PDF and take a screenshot." It's turning a PDF page into a plain image file, on demand, from code. That's what an image-from-PDF endpoint is for: hand it a PDF, tell it which pages you want, and get back JPEG, PNG, or TIFF files you can display, store, or ship anywhere an image belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a different job than "convert PDF to something else"
&lt;/h2&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/create-image-from-pdf/" rel="noopener noreferrer"&gt;Create Image from PDF&lt;/a&gt; endpoint does one specific thing well: it renders PDF pages as high-resolution image files. That sounds close to a format conversion, but the use case is really different from converting a PDF to Word or Excel. You're not trying to preserve editable text or table structure. You're trying to preserve &lt;em&gt;how the page looks&lt;/em&gt;, pixel for pixel, so it can be dropped into a context that has no idea what a PDF even is.&lt;/p&gt;

&lt;p&gt;That distinction matters because it changes what "correct" means. A converted Word document is correct if the paragraphs and headings map cleanly. A rendered image is correct if it looks like the page did in a PDF viewer: same layout, same fonts rendered as shapes rather than text, same colors. It's the difference between re-authoring a document and photographing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three formats, and when each one earns its keep
&lt;/h2&gt;

&lt;p&gt;The endpoint supports JPEG, PNG, and TIFF output, and the choice isn't cosmetic.&lt;/p&gt;

&lt;p&gt;JPEG is the right call when the page looks more like a photograph than a form: scanned documents, pages with embedded photos, anything where a little lossy compression is invisible to the eye but saves real bytes. It's also usually the smallest file of the three, which matters if you're generating a thumbnail grid for hundreds of documents and don't want to pay for storage and bandwidth you don't need.&lt;/p&gt;

&lt;p&gt;PNG is the better default for text-heavy pages, invoices, contracts, anything where crisp edges on small type actually matter. PNG's lossless compression keeps thin lines and small fonts from turning fuzzy the way JPEG artifacts can. If a human is going to zoom in on the image to actually read it, PNG is usually worth the extra file size.&lt;/p&gt;

&lt;p&gt;TIFF is the one most teams reach for last, and usually for a specific reason: archival workflows, print pipelines, or downstream tools that expect uncompressed or losslessly compressed image data as an input. If nothing in your stack specifically asks for TIFF, you probably want JPEG or PNG instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling the endpoint directly
&lt;/h2&gt;

&lt;p&gt;The REST call follows the same base URL, headers, and authentication pattern as every other PDF4me endpoint, covered in &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to PDF4me API&lt;/a&gt;: a Base64-encoded API key, sent as &lt;code&gt;Authorization: Basic &amp;lt;base64-encoded-key&amp;gt;&lt;/code&gt;, against the &lt;code&gt;https://api.pdf4me.com&lt;/code&gt; base URL.&lt;/p&gt;

&lt;p&gt;The endpoint itself is a POST to &lt;code&gt;/api/v2/CreateImages&lt;/code&gt;. Here's a minimal working example in Python:&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="c1"&gt;# from your dev.pdf4me.com dashboard
&lt;/span&gt;&lt;span class="n"&gt;auth_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="sh"&gt;"&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;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&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="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;statement.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="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;statement-page-1&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;pageNrs&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&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;imageAction&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;WidthPixel&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;1000&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;ImageExtension&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;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;PageSelection&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;PageNrs&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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="n"&gt;auth_header&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/CreateImages&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="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="n"&gt;image_bytes&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;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File Content&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File Name&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_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out_file&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;image_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few field names worth calling out explicitly, since they're easy to get wrong from memory: the source PDF goes in as &lt;code&gt;docContent&lt;/code&gt; (Base64), the output name goes in &lt;code&gt;docname&lt;/code&gt; (lowercase "n"), and the actual rendering options live under &lt;code&gt;imageAction&lt;/code&gt;: &lt;code&gt;WidthPixel&lt;/code&gt; for output resolution, &lt;code&gt;ImageExtension&lt;/code&gt; for the format (&lt;code&gt;jpeg&lt;/code&gt;, &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;tiff&lt;/code&gt;, and a few others), and page selection either as the top-level &lt;code&gt;pageNrs&lt;/code&gt; string (&lt;code&gt;"1"&lt;/code&gt;, &lt;code&gt;"1,3,5"&lt;/code&gt;, &lt;code&gt;"1,2-3,5,7-"&lt;/code&gt;) or the nested &lt;code&gt;imageAction.PageSelection.PageNrs&lt;/code&gt; array, depending on which shape your client library prefers to build. The response hands back &lt;code&gt;File Content&lt;/code&gt; (the Base64 image bytes) and &lt;code&gt;File Name&lt;/code&gt;, both with capitalized, space-separated keys, which is a different naming convention than the request body uses. That inconsistency between request and response field casing isn't a typo in this article, it's how the API actually responds, so build your parsing accordingly.&lt;/p&gt;

&lt;p&gt;If you'd rather see this shape before writing a line of code, the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; lets you try any PDF4me endpoint live in your browser, image rendering included. Upload a real PDF, pick a page, pick a format, and look at exactly what you'll be parsing on the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The no-code path: solid on Make, a short detour everywhere else
&lt;/h2&gt;

&lt;p&gt;If your team builds automations in &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-make/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, there's good news: &lt;a href="https://docs.pdf4me.com/integration/make/image/create-images-from-pdf/" rel="noopener noreferrer"&gt;Create Images from PDF&lt;/a&gt; is a native module. Drop it into a scenario, point it at a PDF from an earlier step, and it hands you image files you can push into the next module, whether that's a Slack notification, a cloud storage upload, or a CRM attachment field.&lt;/p&gt;

&lt;p&gt;Here's the part worth saying plainly instead of glossing over: as of this writing, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-power-automate/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-zapier/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-n8n/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; don't have a dedicated image-from-PDF action built into their PDF4me connectors yet. That's not a dead end, it's just a slightly different route. All three platforms let you call an HTTP or REST action directly, and since Create Image from PDF is a REST endpoint like any other, you connect using the same Basic-auth pattern above and call the endpoint the way you would from custom code, just wired up inside a no-code canvas instead of a script file, using the JSON body shown above as your request payload. It's a few more clicks than a native module, not a different integration approach.&lt;/p&gt;

&lt;p&gt;If you're building something repeatable and the extra setup bothers you, that's a fair signal to raise with your team, not something to route around by hand-rolling image conversion elsewhere in your stack.&lt;/p&gt;

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

&lt;p&gt;The pattern that shows up most often is a document library or portal that needs to show &lt;em&gt;something&lt;/em&gt; before a user commits to opening a full PDF. Thumbnail grids are the obvious example: a list of contracts, applications, or scanned mail, each with a small preview image generated once at upload time and cached, so the viewer never has to render a full PDF just to show a 200-pixel-wide preview.&lt;/p&gt;

&lt;p&gt;Chat and ticketing tools are the second big one. Dropping a rendered PNG of the relevant page into a Slack thread or a support ticket gets a reviewer to the actual content faster than a link they have to click, download, and open elsewhere. It's a small UX difference that adds up across a team that reviews dozens of documents a day.&lt;/p&gt;

&lt;p&gt;Quality assurance is a quieter but real use case too. If your system generates PDFs (invoices, statements, certificates) on a schedule, rendering the first page as an image right after generation gives you a fast, storable visual check: does this look right, before it goes out the door? That's a much lighter check than opening every generated file by hand, and it gives you an artifact you can actually look back at if something goes wrong later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to double-check before you wire this into production
&lt;/h2&gt;

&lt;p&gt;A couple of things are worth confirming for your own use case rather than assuming: which page (or pages) get rendered by default when you don't specify one explicitly, and what resolution the output comes back at for your particular source PDFs and &lt;code&gt;WidthPixel&lt;/code&gt; values. Both are exactly the kind of detail the API Tester above is built for, so you can confirm expected behavior against a real document before it's load-bearing in production code.&lt;/p&gt;

&lt;p&gt;It's also worth deciding format policy once, on purpose, rather than per developer: if your team is going to generate thousands of these images, picking JPEG for scanned/photographic content and PNG for text-heavy pages as a default policy will save you a debugging session later, when someone asks why a contract's fine print looks blurry in a thumbnail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Turning a PDF page into a standalone image isn't a niche need, it shows up anywhere a PDF has to be previewed, shared, or spot-checked without forcing someone to open a full document viewer. PDF4me's Create Image from PDF endpoint handles the rendering over a single &lt;code&gt;POST /api/v2/CreateImages&lt;/code&gt; call; the format you pick (JPEG, PNG, or TIFF) should match what the image is actually for, not just habit. On Make, it's a native module away. Everywhere else, it's a REST call your automation platform's HTTP action can make directly, using the same connection setup you'd use for any other PDF4me integration.&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>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What Do 402 and 429 Errors Mean, and How Do You Handle API Rate Limits Gracefully?</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 18 Sep 2026 19:54:38 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-do-402-and-429-errors-mean-and-how-do-you-handle-api-rate-limits-gracefully-3ein</link>
      <guid>https://dev.to/pdf4me/what-do-402-and-429-errors-mean-and-how-do-you-handle-api-rate-limits-gracefully-3ein</guid>
      <description>&lt;p&gt;Your integration is humming along, then it isn't. A batch job that ran fine yesterday starts throwing errors halfway through, and the first instinct for most developers is to assume something broke on the server side. Sometimes that's true. More often, with a document API, it isn't a bug at all. It's the API telling you, in HTTP status code form, that you've hit a wall it put there on purpose.&lt;/p&gt;

&lt;p&gt;With PDF4me, the two walls you'll run into most often are 402 and 429. They look similar (both are "stop calling me" codes), and it's tempting to treat them the same way in your error-handling code. Don't. They mean different things, they need different fixes, and conflating them is how a five-minute problem turns into a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  402 is a wallet problem
&lt;/h2&gt;

&lt;p&gt;A 402 response means your account has run out of something you're paying for (or something your free plan gives you for free, up to a point). Per PDF4me's own &lt;a href="https://docs.pdf4me.com/general-guidelines/troubleshooting/402-no-credit-daily-limit/" rel="noopener noreferrer"&gt;troubleshooting guide for this exact error&lt;/a&gt;, it shows up in two flavors: you've exhausted your credit balance for the current billing period, or you've hit a daily call cap that certain plans enforce. The fix depends on which one it is, and the dashboard is where you find out, not the error message alone.&lt;/p&gt;

&lt;p&gt;If it's the daily cap, the counter resets the next calendar day, so a scheduled job that fails at 11pm might succeed if you just retry it at midnight. If it's a genuinely empty credit balance, no amount of retrying fixes it. You need to buy more prepaid calls or move to a plan that fits your actual volume.&lt;/p&gt;

&lt;p&gt;The less obvious fix, and the one worth building into your integration from day one rather than after the third 402, is reducing how many calls you're making in the first place. PDF4me's guidance here is straightforward: cache results you already have instead of re-requesting them, batch operations where the endpoint supports it, and eliminate duplicate calls that your own retry logic or a flaky trigger might be generating without you noticing. A workflow that fires the same conversion twice because a webhook retried itself burns exactly the same credits as two genuinely different documents.&lt;/p&gt;

&lt;p&gt;Worth knowing while you're in that part of the docs: there's a related 400 error for exceeding a plan's page limit on a single document, separate from the 402 credit story. If a job fails with "page limit exceeded" rather than a credit message, the fix is splitting the file or upgrading, not topping up credits that were never the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  429 is a pace problem, and PDF4me doesn't publish the exact number
&lt;/h2&gt;

&lt;p&gt;A 429 is a different animal. It doesn't mean you're out of anything. It means you're calling too fast, and the fix is slowing down, not paying up. Per PDF4me's own &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;API connection reference&lt;/a&gt;, which lists the full set of HTTP status codes the V2 REST API can return, 429 is defined plainly as rate limit exceeded, with the recommended fix being exponential backoff and retry.&lt;/p&gt;

&lt;p&gt;Here's the part worth flagging honestly rather than guessing past: PDF4me's public documentation doesn't publish a specific numeric threshold for this, no fixed requests-per-second or requests-per-minute figure you can hardcode a check against. That's not a gap in this article, it's a gap in the source material, and inventing a number would be worse than admitting there isn't one. What that means practically is you shouldn't try to precisely calculate your way under a ceiling you can't see. Build defensively instead: treat 429 as a signal to pause and retry, not a bug to chase down.&lt;/p&gt;

&lt;p&gt;A minimal, honest pattern looks like this in Python:&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;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&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;def&lt;/span&gt; &lt;span class="nf"&gt;call_pdf4me&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="p"&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;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&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;attempt&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="n"&gt;max_retries&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="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;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="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="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gave up after repeated 429 responses&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;Exponential backoff with a bit of random jitter (rather than every failed request retrying at the exact same intervals) is the standard shape for this, and it's what the docs point toward even without a published threshold to tune it against. If your traffic pattern is more "burst of 200 documents at once" than "steady trickle," the better fix often isn't a smarter retry loop at all, it's using PDF4me's asynchronous processing instead of hammering the synchronous endpoint. For large files or batch operations, the API can return a &lt;code&gt;jobId&lt;/code&gt; instead of an immediate result, which you then poll via &lt;code&gt;GET /api/v2/JobResult/{jobId}&lt;/code&gt; until it reports success. That single change, batching through async rather than firing dozens of synchronous requests in a tight loop, quietly removes most of the conditions that produce a 429 in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log the status code, not just "it failed"
&lt;/h2&gt;

&lt;p&gt;A cheap habit that pays off the first time you actually need it: log the raw HTTP status code alongside every failed PDF4me call, not just a generic "request failed" message. Six months into production, "we saw 40 errors last week" tells you nothing. "38 were 429s clustered around our 9am batch job, 2 were genuine 402s from a plan we'd outgrown" tells you exactly what to fix and in which order. This is especially worth doing before you reach for a support ticket. PDF4me's status codes are specific enough (400, 401, 402, 403, 404, 429, 500, each with a distinct meaning) that a log line with just the number, timestamp, and endpoint usually answers "is this us or them" before anyone has to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  They're easy to confuse with 401, so don't
&lt;/h2&gt;

&lt;p&gt;There's a third code worth mentioning in the same breath, if only to rule it out. A &lt;a href="https://docs.pdf4me.com/general-guidelines/troubleshooting/401-unauthorized/" rel="noopener noreferrer"&gt;401 Unauthorized&lt;/a&gt; error also stops your request cold, but for an unrelated reason: a missing, malformed, or revoked API key, most commonly because the Base64-encoded Authorization header is missing its trailing colon. If your error-handling code branches on "did the request fail," it's worth branching further on the actual status code before deciding whether the fix is "wait and retry" (429), "check the dashboard" (402), or "check the key" (401). Treating all three as the same failure mode means you'll spend time debugging a rate-limit issue as if it were a credentials issue, or vice versa.&lt;/p&gt;

&lt;p&gt;PDF4me's broader &lt;a href="https://docs.pdf4me.com/general-guidelines/troubleshooting/" rel="noopener noreferrer"&gt;troubleshooting index&lt;/a&gt; is worth bookmarking for exactly this reason: it's organized by symptom, not buried in a single giant FAQ, which makes it faster to confirm which of these you're actually looking at before you start writing retry logic for the wrong problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're not calling the REST API directly
&lt;/h2&gt;

&lt;p&gt;Not every PDF4me integration hits &lt;code&gt;api.pdf4me.com&lt;/code&gt; from application code. If you're going through &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-power-automate/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-zapier/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-make/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, or &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-n8n/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; instead, the connector sits between you and the raw HTTP layer, but the underlying account limits (credits, daily caps, pace) still apply exactly the same way, since they're account-level, not code-level. What changes is how you see the failure: instead of catching a 429 in a try/except block, you're more likely to see a failed flow run or a stalled Zap, which is a worse debugging experience precisely because the status code is hidden a layer down. PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/troubleshooting/zapier-power-automate-tips/" rel="noopener noreferrer"&gt;Zapier and Power Automate troubleshooting tips&lt;/a&gt; cover several of the no-code-specific gotchas that show up here, like binary data not being recognized correctly between steps, which can look like a rate-limit failure but isn't one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test without spending the calls that matter
&lt;/h2&gt;

&lt;p&gt;One more habit worth building in: you don't need to burn production credits, or risk tripping a 429 on a live pipeline, just to check whether a request shape is correct. PDF4me's &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;interactive API Tester&lt;/a&gt; lets you paste your key, upload a file, and run a real request straight from the browser, which is a much cheaper place to debug a malformed payload than inside a scheduled job that's already mid-retry. And if you're troubleshooting credit consumption specifically, the fastest first step is checking actual usage against your plan in the &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;PDF4me dashboard&lt;/a&gt; rather than guessing from the error message alone. If you haven't generated a key yet, the &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Getting Started guide&lt;/a&gt; walks through creating an account and making your first authenticated request, which is also where you'll find where the dashboard lives day to day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build for both, once, and stop thinking about them
&lt;/h2&gt;

&lt;p&gt;None of this is complicated once it's in place. A 402 needs a dashboard check and either a wait, a top-up, or a plan change. A 429 needs backoff, jitter, and ideally fewer synchronous calls in the first place, via async processing or basic caching. Neither is a sign your integration is broken; they're both the API doing exactly what it's supposed to do when it hits an account-level or pace-level ceiling. Build the handling once, put it in whatever wrapper function or middleware sits between your code and PDF4me's API, and you'll stop noticing these errors at all, which is the actual goal.&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>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to OCR an Image with Python: Extracting Text from Photos and Scans via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 18 Sep 2026 16:29:18 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-ocr-an-image-with-python-extracting-text-from-photos-and-scans-via-api-56dn</link>
      <guid>https://dev.to/pdf4me/how-to-ocr-an-image-with-python-extracting-text-from-photos-and-scans-via-api-56dn</guid>
      <description>&lt;p&gt;Someone on your team just photographed a whiteboard after a planning meeting, or a customer emailed a phone snapshot of a handwritten delivery note, or a field technician uploaded a picture of a nameplate riveted to a machine. In every case, the information you need is sitting inside a JPEG, not a PDF, and not a single word of it is selectable, searchable, or usable by any downstream system. It's pixels pretending to be data.&lt;/p&gt;

&lt;p&gt;This is a narrower problem than "OCR a PDF," and it deserves its own answer instead of a workaround. A scanned PDF at least has a predictable container. A photo does not: it might be rotated, poorly lit, slightly blurred, or shot at an angle, and it arrives as a raw image file, not a document format with pages and metadata. PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/image-extract-text/" rel="noopener noreferrer"&gt;Image Extract Text&lt;/a&gt; endpoint runs OCR directly against that image, no PDF conversion step in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint, exactly as documented
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /api/v2/ImageExtractText&lt;/code&gt; against the base URL &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;. The request body, confirmed against the live docs page, is exactly two 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;"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;"receipt.jpg"&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;"&amp;lt;base64-encoded image bytes&amp;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;&lt;code&gt;docName&lt;/code&gt; is the source filename, extension included (that's how the service knows what format it's looking at). &lt;code&gt;docContent&lt;/code&gt; is the full image, Base64-encoded. The documented supported formats are JPG, PNG, BMP, TIFF, and other popular image formats.&lt;/p&gt;

&lt;p&gt;One honest flag here: the docs page's own JSON response example shows a generic "File Content" placeholder field rather than a fully worked example of the extracted-text payload shape. Treat the exact response field name as something to confirm against your own first live call (the API Tester below is the fastest way to do that) rather than something this article can promise down to the key name.&lt;/p&gt;

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

&lt;p&gt;The shape of the call is simple: read the file, Base64-encode it, POST the JSON payload, and check the response status before trusting the body.&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="c1"&gt;# from your PDF4me developer dashboard
&lt;/span&gt;&lt;span class="n"&gt;ENDPOINT&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/ImageExtractText&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;receipt.jpg&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;image_bytes&lt;/span&gt; &lt;span class="o"&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="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;receipt.jpg&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&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;image_bytes&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="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="c1"&gt;# Check docs.pdf4me.com / your API dashboard for the exact
&lt;/span&gt;    &lt;span class="c1"&gt;# Authorization header scheme (e.g. Basic vs Bearer) tied to your key
&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="n"&gt;API_KEY&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;ENDPOINT&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap this the way you'd wrap any external API call in production: check &lt;code&gt;response.status_code&lt;/code&gt; before you trust the body, log the filename alongside the request so a failure is traceable back to a specific image, and decide up front what "no text found" should mean for your pipeline versus what an outright request failure should mean. A blank whiteboard photographed by mistake and a malformed payload are two very different situations, and code that treats them the same way will make the wrong call on whichever one happens first in production.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.pdf4me.com/apiv2/documentation/actions/image-extract-text/" rel="noopener noreferrer"&gt;PDF4me API v2 documentation for Image Extract Text&lt;/a&gt; lists working code samples across Python, C#, Java, JavaScript, and several no-code/serverless environments (Salesforce, Google Apps Script, AWS Lambda) alongside n8n, so whichever stack you're standardized on, there's a real starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the image itself is the real variable
&lt;/h2&gt;

&lt;p&gt;Every OCR system is only as good as the image you feed it. Resolution and focus matter more than almost anything else, a crisp scan and a motion-blurred phone photo of the same page are not the same input. Orientation is a silent failure mode too: a photo taken sideways doesn't throw an error, it just returns garbage, because the engine is reading pixels, not intent. If your app accepts photos from a phone, normalize orientation using the image's own EXIF data before OCR, not after you're debugging why extraction quality dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits, and where it doesn't yet
&lt;/h2&gt;

&lt;p&gt;As of this writing, Image Extract Text has a documented, ready-made action in &lt;a href="https://docs.pdf4me.com/n8n/image/image-extract-text/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; (supporting JPEG, PNG, GIF, and BMP up to 50MB, with both JSON and plain-text output shapes), but no dedicated Power Automate, Zapier, or Make page of its own. If your stack is one of those three, the REST endpoint above is callable from any of their generic HTTP-request actions today, it just isn't wrapped in a purpose-built connector step yet.&lt;/p&gt;

&lt;p&gt;Before wiring this into anything, validate your payload shape with the &lt;a href="https://docs.pdf4me.com/url-api-tester/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt;, and use 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; to get your API key and first authenticated call working.&lt;/p&gt;

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

&lt;p&gt;OCR is pattern recognition against pixels, not comprehension. It will hand you back the characters it's confident it saw, it will not tell you that the total on a receipt looks wrong or that the text is in a language your downstream system doesn't expect. Build your validation logic to expect imperfect input, especially on low-quality or handwritten source images, rather than treating extracted text as ground truth the moment it arrives.&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>python</category>
      <category>ocr</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From Inbox to Expense Report: Classifying, Extracting, and Merging Receipts in Three API Calls</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 17 Sep 2026 09:15:30 +0000</pubDate>
      <link>https://dev.to/pdf4me/from-inbox-to-expense-report-classifying-extracting-and-merging-receipts-in-three-api-calls-217h</link>
      <guid>https://dev.to/pdf4me/from-inbox-to-expense-report-classifying-extracting-and-merging-receipts-in-three-api-calls-217h</guid>
      <description>&lt;p&gt;Somewhere in your company right now, someone is opening twelve emails to find seven receipts, squinting at a phone photo of a fuel pump display, and retyping a hotel folio into a spreadsheet by hand. Expense reporting is one of the last places in the modern back office where "automation" still means a human being a very slow, very literal-minded parser.&lt;/p&gt;

&lt;p&gt;The frustrating part is that the underlying problem is not hard. A receipt is a document. It has a type. It has a merchant, a date, some line items, a tax figure, and a total. Once you can get software to read those five things reliably, the rest is plumbing. This is a plumbing problem, and it takes exactly three API calls to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Expense automation usually falls apart at one of three points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sorting.&lt;/strong&gt; The inbox has invoices, contracts, and receipts all mixed together, and someone has to figure out which is which before anything else can happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading.&lt;/strong&gt; Even once you know something is a receipt, pulling the merchant name, the line items, and the total out of a photo or a scanned PDF reliably is its own project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging.&lt;/strong&gt; Finance does not want forty separate PDF attachments per expense report. They want one file, in order, that they can archive against the reimbursement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three PDF4me endpoints map directly onto those three points: &lt;a href="https://docs.pdf4me.com/pdf4me-api/extract/classify-document/" rel="noopener noreferrer"&gt;Classify Document&lt;/a&gt; for sorting, the AI Receipt Parser for reading, and &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/merge/" rel="noopener noreferrer"&gt;Merge Multiple PDFs&lt;/a&gt; for packaging. Wire them together in order and you have gone from "pile of attachments" to "structured expense report plus one clean archive PDF" without a human touching a single line item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: know what you're looking at before you extract anything
&lt;/h2&gt;

&lt;p&gt;Every automated intake pipeline eventually receives something that isn't what it expected. An employee forwards a signed contract to the expenses inbox by mistake, or attaches an invoice from a vendor instead of their own reimbursable receipt. If your pipeline runs receipt extraction on all of it blindly, you get garbage fields back with no warning that the document was never a receipt in the first place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/extract/classify-document/" rel="noopener noreferrer"&gt;Classify Document&lt;/a&gt; is a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/api/v2/ClassifyDocument&lt;/code&gt; that takes a Base64-encoded file (&lt;code&gt;docContent&lt;/code&gt;) and its filename (&lt;code&gt;docName&lt;/code&gt;) and returns a predicted &lt;code&gt;documentType&lt;/code&gt; (invoice, contract, receipt, and so on), a &lt;code&gt;category&lt;/code&gt;, a &lt;code&gt;confidence&lt;/code&gt; score, and basic &lt;code&gt;metadata&lt;/code&gt; like page count. Set the optional &lt;code&gt;async&lt;/code&gt; flag to &lt;code&gt;true&lt;/code&gt; and large batches get a &lt;code&gt;202 Accepted&lt;/code&gt; with a &lt;code&gt;Location&lt;/code&gt; header to poll instead of holding the connection open.&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;inbox-attachment.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;inbox-attachment.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;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;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_BASE64_ENCODED_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ClassifyDocument&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="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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&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;That confidence score matters more than it looks like it should. It's the field that decides whether an attachment goes straight into the receipt-parsing step below or gets routed to a human for a second look. A pipeline that skips this gate isn't actually automated, it's just automation-shaped, quietly feeding it whatever lands in the inbox and hoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: turn a receipt into structured data, not a wall of OCR text
&lt;/h2&gt;

&lt;p&gt;Once you know you're holding a receipt, the actual extraction is a pre-tuned, no-code endpoint rather than a general-purpose parser you have to configure yourself: the &lt;strong&gt;AI Receipt Parser&lt;/strong&gt;. It's available directly inside &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-receipt-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-receipt/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-receipt/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, which means the version of this pipeline that never touches raw REST calls at all is a completely reasonable way to build it, if your team already lives in one of those tools.&lt;/p&gt;

&lt;p&gt;Feed it a receipt as binary data, a Base64 string, or a URL, along with a filename PDF4me uses for format detection (PDF, PNG, JPG, and JPEG are all fair game). What comes back is not a transcript, it's a schema: merchant name, address, phone, and website; a line-items array with quantity, unit price, and category per item; subtotal, tax, total amount, and payment method; and a receipt-type-specific field or two depending on what you're looking at (a &lt;code&gt;roomNumber&lt;/code&gt; on a hotel folio, a fuel type on a gas station receipt).&lt;/p&gt;

&lt;p&gt;Two details are worth building your logic around instead of ignoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receipt Type is optional but not decorative.&lt;/strong&gt; Passing a hint like "meal," "hotel," "fuel," "healthcare," or "training" measurably improves extraction accuracy and unlocks the category-specific fields above. If you already know the expense category from context (a corporate card feed, a policy tag), pass it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;warnings&lt;/code&gt; and &lt;code&gt;fallbackUsed&lt;/code&gt; are your real confidence signal.&lt;/strong&gt; The parser doesn't just hand back numbers and hope for the best. When something was ambiguous or estimated, it tells you. Route anything carrying a warning to a review queue instead of straight into the general ledger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For receipts specifically, the pre-tuned parser above already covers the fields finance actually asks for, including the optional custom-field keys for anything genuinely one-off, like a loyalty program number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three: give finance one file, not forty
&lt;/h2&gt;

&lt;p&gt;You now have structured expense data flowing wherever your ledger lives. What you still have is a pile of original receipt files that need to survive as an audit trail. Nobody downstream wants forty individual attachments per report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/merge/" rel="noopener noreferrer"&gt;Merge Multiple PDFs&lt;/a&gt; is the plainest endpoint in this pipeline and does exactly what the name says: &lt;code&gt;POST&lt;/code&gt; an array of Base64-encoded PDFs to &lt;code&gt;docContent&lt;/code&gt; (minimum two), a &lt;code&gt;docName&lt;/code&gt; for the result, and it concatenates them in the exact order the array was in. The array order is the merge order, full stop, so if sequencing matters (chronological, by amount, by category), sort before you call it, not after. The synchronous response is the merged PDF as raw bytes, not JSON. Write those bytes straight to a file. There's an &lt;code&gt;async&lt;/code&gt; flag here too, for merging large batches without holding a connection open while it works.&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;def&lt;/span&gt; &lt;span class="nf"&gt;to_base64&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="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;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="k"&gt;return&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="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;to_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;receipt-01.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;to_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;receipt-02.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;to_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;receipt-03.pdf&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;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;expense-batch-merged.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;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;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_BASE64_ENCODED_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/Merge&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;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;expense-batch-merged.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your receipts arrive as images rather than PDFs, that's a one-step conversion before this call, not a blocker; the point stands that whatever format they land in, this is the step that turns "forty attachments" into "one exhibit."&lt;/p&gt;

&lt;p&gt;This isn't REST-only, either. &lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; exposes it as an action that takes File Contents 1 and 2 as required inputs and lets you add more slots on demand. &lt;a href="https://docs.pdf4me.com/integration/zapier/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; takes a mappable list of files (or direct URLs) and merges them in the order you list them, no separate sort field, which is exactly the ordering behavior above stated a different way. &lt;a href="https://docs.pdf4me.com/integration/make/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; adds a genuinely useful option the others don't: &lt;strong&gt;Skip Protected PDFs&lt;/strong&gt;, so a password-protected receipt scan doesn't silently break the whole batch. &lt;a href="https://docs.pdf4me.com/integration/n8n/merge-split/merge-multiple-pdfs/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; returns &lt;code&gt;fileName&lt;/code&gt;, &lt;code&gt;mimeType&lt;/code&gt;, &lt;code&gt;fileSize&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, and &lt;code&gt;inputFileCount&lt;/code&gt;, which is exactly the kind of structured confirmation you want logged before a merge job is allowed to mark an expense report "closed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the three calls in a row
&lt;/h2&gt;

&lt;p&gt;The pipeline, end to end, looks like this: an attachment lands in a shared inbox or a designated cloud folder. Classify Document decides whether it's actually a receipt, and if the confidence score is low, it goes to a human instead of the next step. If it clears that bar, the AI Receipt Parser turns it into merchant, line items, totals, and payment method, tagged with whatever receipt type you already know from context, with any &lt;code&gt;warnings&lt;/code&gt; flagged for review rather than trusted blindly. In parallel, the same original file gets queued for the batch, and once a reporting period (a week, a trip, a month) closes, Merge Multiple PDFs stitches every receipt in that batch into one archive PDF, in whatever order finance actually wants it filed.&lt;/p&gt;

&lt;p&gt;The employee who forwarded twelve emails never touches a spreadsheet. Finance gets clean structured data and one attachment instead of forty. And the whole thing is three API calls that happen to already exist, wired together in the order the problem actually unfolds.&lt;/p&gt;

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

&lt;p&gt;None of this replaces your expense policy engine, and it shouldn't. Classification and extraction tell you what a document is and what it says; they don't decide whether a $400 dinner is within policy. Build the approval logic you already have around this pipeline, not instead of it. And treat every &lt;code&gt;warnings&lt;/code&gt; flag and every sub-threshold classification &lt;code&gt;confidence&lt;/code&gt; score as exactly what it is: a signal that a human should look before the number hits the books.&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>python</category>
      <category>automation</category>
      <category>ai</category>
    </item>
    <item>
      <title>Resize Percentage Tops Out at 100 on One Platform, and Doubles on Four Others</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:56:25 +0000</pubDate>
      <link>https://dev.to/pdf4me/resize-percentage-tops-out-at-100-on-one-platform-and-doubles-on-four-others-3pe9</link>
      <guid>https://dev.to/pdf4me/resize-percentage-tops-out-at-100-on-one-platform-and-doubles-on-four-others-3pe9</guid>
      <description>&lt;p&gt;Resize Image looks like the simplest endpoint in PDF4me's image toolkit. Give it a percentage, get a smaller or bigger file back. That assumption holds on four of the five surfaces this feature ships through. It does not hold on the fifth, and the gap only shows up once a workflow tries to enlarge an image instead of shrink one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "resize by percentage" is supposed to mean
&lt;/h2&gt;

&lt;p&gt;Across &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/resize-image/" rel="noopener noreferrer"&gt;Resize Image on the REST API&lt;/a&gt;, the endpoint accepts a resize type and, for percentage mode, a single numeric value applied to both dimensions of the source image. The plain reading of "percentage" is symmetrical: a value under 100 shrinks the image, a value over 100 enlarges it, 100 leaves it unchanged. That is exactly how &lt;a href="https://docs.pdf4me.com/integration/power-automate/image/resize-image/" rel="noopener noreferrer"&gt;Power Automate's Resize Image action&lt;/a&gt; documents it. The parameter table's own worked example spells out both directions at once: 50 for half size, 200 for double. &lt;a href="https://docs.pdf4me.com/integration/zapier/image/resize-image/" rel="noopener noreferrer"&gt;Zapier's Resize Image step&lt;/a&gt; carries the identical worked example, 50 for half size, 200 for double, word for word. &lt;a href="https://docs.pdf4me.com/integration/n8n/image/resize-image/" rel="noopener noreferrer"&gt;n8n's Resize Image node&lt;/a&gt; states the same behavior in its own language: below 100 reduces the image, above 100 enlarges it. Its FAQ section goes a step further and directly confirms that pushing the value past 100 increases the output's pixel dimensions, while also noting the honest limit that comes with any upscale, that resizing cannot recover detail the original capture never had. Three separate integration platforms, three separate documentation pages, one consistent behavior: percentage mode is a two-way dial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The platform where it is not
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/image/resize-image/" rel="noopener noreferrer"&gt;Make's Resize Image module&lt;/a&gt; documents a hard ceiling. Its Important Facts section states the percentage field caps at 100. Its Practical Tips section repeats the same constraint in different words. Its FAQ section addresses the upscale question directly and answers it the same way a third time: scaling up is not supported, because the field caps at 100. That is not a rounding footnote or a minor phrasing difference from the other three platforms. It is the opposite behavior for the same field, on the same underlying feature, described three separate times on the same page, which rules out a documentation typo and leaves an actual product constraint on that one integration surface.&lt;/p&gt;

&lt;p&gt;Put the four pages next to each other and the split is clean. Power Automate: 50 to 200 range, worked example included. Zapier: 50 to 200 range, identical worked example. n8n: below 100 shrinks, above 100 enlarges, FAQ-confirmed. Make: capped at 100, no upscale path through this field, stated three times on one page. A workflow builder who tests the enlarge case on Power Automate, Zapier, or n8n and then rebuilds the same flow on Make will hit a silent ceiling that none of those other three platforms have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual REST request, verified against the live sample
&lt;/h2&gt;

&lt;p&gt;The REST payload's field names are worth pinning down exactly, since the docs page's own JSON example and the official Python sample in &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;the pdf4me-api-samples repo&lt;/a&gt; agree on everything except one field the marketing payload leaves out:&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;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/ResizeImage?schemaVal=Percentange&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.jpg&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;image_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;sample.jpg&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;image_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;ImageResizeType&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;Percentage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# or "Specific"
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResizePercentage&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.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# decimal as string
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# used when ImageResizeType is Specific
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MaintainAspectRatio&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;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;# not in the docs page's JSON example, present in the official sample
&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;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 200: resized image returned directly in the response body
# 202: async job accepted, poll the Location header URL until it returns 200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth flagging in that payload. First, the query string's own default value is &lt;code&gt;schemaVal=Percentange&lt;/code&gt;, not &lt;code&gt;Percentage&lt;/code&gt;, a literal misspelling that appears verbatim across the docs page, the C# and Python samples, and the Salesforce class in the sample repo. Copy it exactly as published rather than correcting the spelling. Second, &lt;code&gt;isAsync&lt;/code&gt; does not appear in the docs page's own "Payload" JSON example, only in the actual Python sample script. Anyone copying the docs page's example verbatim will get a payload that works but silently defaults async handling rather than declaring it, which matters if a caller wants to rely on the 202-plus-polling path documented in the sample script's own status-code handling. The &lt;a href="https://docs.pdf4me.com/url-api-tester/resize-image/" rel="noopener noreferrer"&gt;Resize Image entry in the interactive API Tester&lt;/a&gt; is the fastest way to confirm the exact parameter casing a specific account's endpoint expects before writing code against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than a single mismatched field
&lt;/h2&gt;

&lt;p&gt;Document automation pipelines get built once and reused for months. A logistics team pulling label images off a scanner, an e-commerce operator normalizing product photos before they hit a marketplace feed, a finance team standardizing scanned receipts before an AI parser reads them, all of these are exactly the kind of repeatable image step this endpoint exists for. If the pipeline was prototyped on Zapier or Power Automate with a 150 or 200 percent value to upscale a low-resolution source, and the same logic later gets ported to Make because that is the automation tool the rest of the org standardized on, the enlarge step does not error loudly. It just does not enlarge. The field accepts the value, caps it at 100 internally per Make's own documentation, and the output comes back at original size. That is a much harder failure to catch in a screenshot review than an outright error message would be, because nothing in the run log necessarily flags it as a problem. It looks like the step ran successfully. It just did not do what the same configuration would have done on a different platform.&lt;/p&gt;

&lt;p&gt;This is also a case where checking the REST API page alone would not have caught the discrepancy. The REST documentation describes percentage mode generically, the same way Power Automate, Zapier, and n8n do, without calling out any upper limit. The cap lives specifically in how Make's module wraps that underlying call, not in the API contract itself. Anyone building on Make needs Make's own page open, not just the REST reference, before assuming the field behaves the way it does everywhere else PDF4me exposes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more asymmetry: Power Automate's extra resize mode
&lt;/h2&gt;

&lt;p&gt;Power Automate's Resize Image action documents a third resize type beyond percentage and fixed pixel dimensions: Max Dimensions, which takes separate MaxWidth and MaxHeight fields and scales the image down to fit inside that bounding box while preserving aspect ratio. Nothing in the REST API page, the Make module page, the Zapier step page, or the n8n node page describes an equivalent third mode. That does not mean the underlying capability is impossible to replicate elsewhere, only that it is not documented as a first-class option outside Power Automate's action today. A team standardizing a multi-platform image pipeline and relying on Max Dimensions logic inside Power Automate should not assume the same named mode exists if that workflow gets rebuilt on Make, Zapier, or n8n. It would need to be approximated with the fixed-dimension or percentage modes those platforms do document, calculated manually against the source image's known size.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually check before building on this endpoint
&lt;/h2&gt;

&lt;p&gt;None of this is a case for avoiding Resize Image. It is a case for reading the specific platform's own page before assuming percentage mode behaves identically everywhere PDF4me exposes it, because on this endpoint, for this one direction of the operation, it does not. Four platforms agree, one does not, and the disagreement is buried in a parameter table and an FAQ section rather than surfaced anywhere a workflow builder would see it by default. For any pipeline that needs to enlarge an image and not just shrink one, confirming which platform is running the resize step is worth the thirty seconds it takes, before a value like 150 or 200 gets typed in and quietly does nothing.&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>webdev</category>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Extract Form Data Gives You Isn't What Fill a PDF Form Wants Back</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:54:06 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-extract-form-data-gives-you-isnt-what-fill-a-pdf-form-wants-back-5b7</link>
      <guid>https://dev.to/pdf4me/what-extract-form-data-gives-you-isnt-what-fill-a-pdf-form-wants-back-5b7</guid>
      <description>&lt;p&gt;A form has three moments in its life on PDF4me: someone builds the fillable fields, someone (or something) fills them, and someone reads the filled values back out. &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; and &lt;a href="https://docs.pdf4me.com/pdf4me-api/extract/extract-form-data-from-pdf/" rel="noopener noreferrer"&gt;Extract Form Data from PDF&lt;/a&gt; sit on either side of that middle step, and PDF4me's own documentation calls them companions. The Fill endpoint's own docs page lists Extract as a "Related action" and describes it as the way to "pull the field names and current values out of any AcroForm PDF before designing your dataArray payload." The n8n integration goes further and calls Fill "the reverse operation" of Extract. That is the right mental model for what these two endpoints do. It is not a reliable guide to what they return, because on every platform PDF4me exposes this pair through, the shape Extract hands back is not the shape Fill expects to receive. Round-tripping one into the other means writing a translation step yourself, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  REST: a flat string in, a typed array out
&lt;/h2&gt;

&lt;p&gt;On the REST API, Fill a PDF Form (&lt;code&gt;POST /api/v2/FillPdfForm&lt;/code&gt;) takes a &lt;code&gt;dataArray&lt;/code&gt; field that has to be a stringified JSON object, not a nested object, with keys that match the target PDF's AcroForm field names exactly: &lt;code&gt;"dataArray": "{\"firstname\": \"John\", \"lastname\": \"Doe\"}"&lt;/code&gt;. Send a real JSON object instead of a string and the API returns a deserialization error. Extract Form Data from PDF (&lt;code&gt;POST /api/v2/ExtractPdfFormData&lt;/code&gt;), by contrast, returns a &lt;code&gt;formFields&lt;/code&gt; array, one object per field, each with &lt;code&gt;fieldName&lt;/code&gt;, &lt;code&gt;fieldValue&lt;/code&gt;, and &lt;code&gt;fieldType&lt;/code&gt; as separate keys. Take that array straight out of Extract's response and post it back into Fill's &lt;code&gt;dataArray&lt;/code&gt; and it will not work. Fill wants one flat object of name-to-value pairs. Extract gives you an array of three-key objects. Getting from one to the other means a short loop that drops &lt;code&gt;fieldType&lt;/code&gt; and collapses the array into a single object, not a direct pass-through.&lt;/p&gt;

&lt;p&gt;There is a second wrinkle worth flagging on this same pair, and it sits inside PDF4me's own documentation rather than between platforms. The REST docs page for Fill a PDF Form lists &lt;code&gt;dataArray&lt;/code&gt; as the required field for supplying values. The &lt;a href="https://docs.pdf4me.com/url-api-tester/fill-pdf-form/" rel="noopener noreferrer"&gt;Fill PDF Form entry in the interactive API Tester&lt;/a&gt; lists both &lt;code&gt;dataArray&lt;/code&gt; and &lt;code&gt;InputFormData&lt;/code&gt;, an array of &lt;code&gt;{fieldName, fieldValue}&lt;/code&gt; objects, as required, and its own sample request sends both fields populated with the same data at once. The two pages describing the identical endpoint do not agree on what a caller has to send. Worth testing directly before assuming either page alone: the &lt;a href="https://docs.pdf4me.com/url-api-tester/extract-form-data-from-pdf/" rel="noopener noreferrer"&gt;Extract Form Data from PDF tester&lt;/a&gt; is the fastest way to see the real output shape on a sample file before writing a single line of Fill code against it.&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Extract Form Data returns an array of typed field objects
&lt;/span&gt;&lt;span class="n"&gt;extract_resp&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/ExtractPdfFormData&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;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;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;filled.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&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="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;filled.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="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="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;# formFields: [{"fieldName": "firstname", "fieldValue": "John", "fieldType": "text"}, ...]
&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Fill a PDF Form wants one flat object, stringified, no fieldType
&lt;/span&gt;&lt;span class="n"&gt;reshaped&lt;/span&gt; &lt;span class="o"&gt;=&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fieldName&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fieldValue&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;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;extract_resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formFields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="n"&gt;fill_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;templateDocName&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;blank_template.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;templateDocContent&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&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="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;blank_template.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="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;dataArray&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reshaped&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputDataType&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;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;outputType&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;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;False&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make: two ways in, one array out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/forms/fill-pdf-form/" rel="noopener noreferrer"&gt;Fill a PDF Form in Make&lt;/a&gt; offers a choice most other platforms do not: Map fields, a repeatable list of &lt;code&gt;Field Name&lt;/code&gt; / &lt;code&gt;Field Value&lt;/code&gt; rows built by hand in the scenario editor, or Json, a single payload for when the data already arrives as an object from a database or API call. &lt;a href="https://docs.pdf4me.com/integration/make/extract/extract-pdf-form-data/" rel="noopener noreferrer"&gt;Extract PDF Form Data in Make&lt;/a&gt; returns one output field, &lt;code&gt;Form Fields&lt;/code&gt;, described as "every extracted field name and its filled values as key-value pairs." Make's own documentation adds a detail that matters more in practice than the shape question: field names "come from whatever the PDF creator named them, often technical IDs like &lt;code&gt;field_001&lt;/code&gt; rather than readable labels," and recommends testing with a sample PDF before building any downstream logic around them. So even once the shape is reconciled, the field names themselves may need a lookup table before they mean anything to a human reading the scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zapier: typed conventions in, a string pair out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/fill-a-pdf-form/" rel="noopener noreferrer"&gt;Fill a PDF Form in Zapier&lt;/a&gt; takes a single &lt;code&gt;Input Data&lt;/code&gt; field, a JSON string, but the value conventions inside it change by field type: &lt;code&gt;{"fieldName":"value"}&lt;/code&gt; for text, &lt;code&gt;{"checkbox":true}&lt;/code&gt; for a checkbox, &lt;code&gt;{"radio":"option2"}&lt;/code&gt; for a radio button, &lt;code&gt;{"list":"Option 2, Option 5"}&lt;/code&gt; for a multi-select. &lt;a href="https://docs.pdf4me.com/integration/zapier/extract/extract-for-data-from-pdf-document/" rel="noopener noreferrer"&gt;Extract Form Data from PDF in Zapier&lt;/a&gt; does not hand back typed values at all. It returns two separate string outputs, &lt;code&gt;Form Data&lt;/code&gt; and &lt;code&gt;Form Data JSON&lt;/code&gt;, neither of which is documented as preserving the checkbox-as-boolean or radio-as-string conventions Fill expects on the way back in. A Zap that reads a filled form and tries to refill a second copy of the same template needs to parse &lt;code&gt;Form Data JSON&lt;/code&gt; and rebuild the type-specific values Fill's own docs describe, not just forward the extracted string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Power Automate: bulk-capable in, thinly documented out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf/fill-pdf-form/" rel="noopener noreferrer"&gt;Fill a PDF Form in Power Automate&lt;/a&gt; has a capability none of the other four surfaces advertise on their Fill page: its &lt;code&gt;Data string&lt;/code&gt; field accepts either a single JSON object, for one filled document, or a JSON array of objects, for generating multiple filled documents from one flow run. &lt;a href="https://docs.pdf4me.com/integration/power-automate/extract/extract-form-data-pdf/" rel="noopener noreferrer"&gt;Extract Form Data from PDF in Power Automate&lt;/a&gt;, on the other hand, has the thinnest documented output of any surface checked here. Its own parameter table lists exactly one output field, &lt;code&gt;Trace ID&lt;/code&gt;, a tracking identifier, with no row for the extracted field data itself. The linked workflow example on that same page shows a flow reading a &lt;code&gt;formData&lt;/code&gt; JSON output further downstream, so the data clearly comes back, it just is not represented in the page's own Output table the way it is on REST, Make, Zapier, or n8n. Anyone wiring this up in Power Automate should expect to find the real output field by running the action once and inspecting the raw response, not by trusting the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n: three input modes in, a nested object out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/forms/fill-a-pdf-form/" rel="noopener noreferrer"&gt;Fill a PDF Form in n8n&lt;/a&gt; supports the richest input surface of the five: a stringified JSON object (matching REST's &lt;code&gt;dataArray&lt;/code&gt; convention under the hood), a JSON file passed as binary data, a base64-encoded JSON blob, or manually entered Field Name / Field Value rows for small, fixed field sets. Its own documentation is blunt about the one rule that holds across every mode: "Form Data (JSON) expects one object, not a list. Send a single JSON object. An array with one object is unwrapped; multiple objects are rejected." &lt;a href="https://docs.pdf4me.com/integration/n8n/extract/extract-form-data-from-pdf/" rel="noopener noreferrer"&gt;Extract Form Data from PDF in n8n&lt;/a&gt; returns a nested &lt;code&gt;formData&lt;/code&gt; object, &lt;code&gt;{"formData": {"name": "PDF4me", "email": "", "country": "USA"}}&lt;/code&gt;, and flags two behaviors worth knowing before building on it: an unfilled field comes back as an empty string rather than being left out of the object entirely, and a checkbox returns whatever export value the form's original designer assigned it, not a guaranteed &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;. n8n's own page for Extract even lays out a small decision table distinguishing this node from plain text extraction and from AI-based parsing, useful context for picking the right node before assuming this one applies to a scanned or flattened document, which it explicitly does not read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that actually does carry across every platform
&lt;/h2&gt;

&lt;p&gt;Field names are the connective tissue, and they are the one part of this pair that behaves consistently everywhere: whatever name a field was given when the AcroForm structure was built is the exact, case-sensitive string every platform's Fill action needs on the way in, and the exact string every platform's Extract action hands back on the way out. Every platform's own documentation says some version of "run Extract first, or inspect the template in Acrobat, to get the real field names" before attempting a Fill call blind. That is good advice, and it is also an implicit admission that the shape mismatch above is expected, not a bug: these are two operations that talk about the same fields using different data shapes, on every single surface, and the fix is always the same small piece of glue code, not a platform-specific workaround.&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>forms</category>
      <category>automation</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Five Platforms, Five Different Ways to Password-Protect the Same PDF</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:10:06 +0000</pubDate>
      <link>https://dev.to/pdf4me/five-platforms-five-different-ways-to-password-protect-the-same-pdf-848</link>
      <guid>https://dev.to/pdf4me/five-platforms-five-different-ways-to-password-protect-the-same-pdf-848</guid>
      <description>&lt;p&gt;A PDF password feature sounds like it should take the same two inputs everywhere: a password, and a setting for what stays allowed once the file is open. PDF4me exposes that feature through five surfaces, the REST API and four no-code connectors, and no two of them ask for it the same way. Some split the password into two separate fields. Some list a different number of permission options, under different names, than the others. One drops the permission choice entirely. Build against one surface and assume a second is a thin wrapper around the same shape, and the first integration attempt will prove that assumption wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two locks on REST, and a naming gap worth catching early
&lt;/h2&gt;

&lt;p&gt;&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;) takes a &lt;code&gt;password&lt;/code&gt;, which gates whether the file opens at all, and a &lt;code&gt;pdfPermission&lt;/code&gt; value, which gates what happens after it opens. They are separate locks doing separate jobs. The docs page flags &lt;code&gt;pdfPermission&lt;/code&gt; as an allow-list rather than a deny-list: naming &lt;code&gt;Fill Forms&lt;/code&gt; opens exactly that one door and closes every other one, printing and copying included, not just the specific action a developer meant to restrict. Check the request against the eight documented values (&lt;code&gt;All&lt;/code&gt;, &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;Copy&lt;/code&gt;, &lt;code&gt;Annotate&lt;/code&gt;, &lt;code&gt;Fill Forms&lt;/code&gt;, &lt;code&gt;Support Disabilities&lt;/code&gt;, &lt;code&gt;Assemble&lt;/code&gt;, &lt;code&gt;Digital Print&lt;/code&gt;) rather than assuming a short list behaves like a checklist of things to block.&lt;/p&gt;

&lt;p&gt;There is a second gap on this same endpoint, easy to miss and cheap to fix. The docs page's parameter table labels the async flag &lt;code&gt;async&lt;/code&gt;, but the official Python sample in &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt; sends it as &lt;code&gt;isAsync&lt;/code&gt; in the actual request payload. Code built against the table's field name instead of the sample's working payload will silently fail to trigger async processing. A call built from the verified payload shape looks like this:&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="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;confidential.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="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;confidential.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;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;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/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;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;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="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;protected.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A synchronous call returns the encrypted PDF as raw binary bytes, not a JSON wrapper, which is why the code above writes &lt;code&gt;response.content&lt;/code&gt; straight to disk. Setting &lt;code&gt;isAsync&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; instead returns an HTTP 202 with a &lt;code&gt;Location&lt;/code&gt; header to poll, worth using for larger files. The encryption itself is AES, at 128-bit or 256-bit strength depending on the PDF specification's own rules for the permission set chosen, applied automatically rather than picked by the caller. That is a capability fact, not a compliance claim: the endpoint encrypts the file and certifies nothing about how it is used afterward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/security/unlock-pdf/" rel="noopener noreferrer"&gt;Unlock PDF&lt;/a&gt; (&lt;code&gt;POST /api/v2/Unlock&lt;/code&gt;) is the other half of the pair, and PDF4me's own "Related actions" copy calls it exactly that: the inverse of Protect, for removing a password already known. It is not a recovery tool. Feed it the wrong password and it fails. Feed it the right one, whether that password was originally set as an opening password or a permissions password, and the returned PDF drops both kinds of restriction in one pass. The REST Unlock page is noticeably thinner than Protect's own: no "Important Facts" callout, no FAQ, and its &lt;a href="https://docs.pdf4me.com/url-api-tester/unlock-pdf/" rel="noopener noreferrer"&gt;API Tester page&lt;/a&gt; is a plain three-field form (&lt;code&gt;docContent&lt;/code&gt;, &lt;code&gt;docName&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;) next to Protect's more built-out &lt;a href="https://docs.pdf4me.com/url-api-tester/protect-document/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five surfaces, five shapes
&lt;/h2&gt;

&lt;p&gt;This is where the real divergence lives, and it is not documented anywhere as a single comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Password fields&lt;/th&gt;
&lt;th&gt;Permission enum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;password&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;8 values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power Automate&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;Password&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;9 values (adds "Print and Modify")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;Password&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;7 values, renamed (no &lt;code&gt;None&lt;/code&gt;, no &lt;code&gt;Support Disabilities&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make&lt;/td&gt;
&lt;td&gt;2 (User + Owner)&lt;/td&gt;
&lt;td&gt;8 values, matches REST exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier&lt;/td&gt;
&lt;td&gt;2 (User + Owner)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/security/protect-document/" rel="noopener noreferrer"&gt;Protect Document in Power Automate&lt;/a&gt; keeps REST's shape closely: one &lt;code&gt;Password&lt;/code&gt; field, one permission selector. But that selector carries nine values instead of eight, adding &lt;code&gt;Print and Modify&lt;/code&gt; as a preset that exists nowhere else in this feature's documentation. &lt;a href="https://docs.pdf4me.com/integration/power-automate/security/unlock-pdf/" rel="noopener noreferrer"&gt;Unlock PDF in Power Automate&lt;/a&gt; mirrors REST's Unlock just as closely, again a single &lt;code&gt;Password&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/security/protect-document/" rel="noopener noreferrer"&gt;n8n's Protect Document&lt;/a&gt; also keeps the single-password shape, but its permission list diverges by name, not just by count: &lt;code&gt;All&lt;/code&gt;, &lt;code&gt;Print&lt;/code&gt;, &lt;code&gt;Copy&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, &lt;code&gt;Fill Forms&lt;/code&gt;, &lt;code&gt;Comment&lt;/code&gt;, &lt;code&gt;Assemble&lt;/code&gt;. Seven values, no &lt;code&gt;None&lt;/code&gt;, no &lt;code&gt;Support Disabilities&lt;/code&gt;, and &lt;code&gt;Print&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, and &lt;code&gt;Comment&lt;/code&gt; do not map cleanly onto REST's &lt;code&gt;Digital Print&lt;/code&gt; and &lt;code&gt;Annotate&lt;/code&gt; wording. &lt;a href="https://docs.pdf4me.com/integration/n8n/security/unlock-pdf/" rel="noopener noreferrer"&gt;Unlock PDF in n8n&lt;/a&gt; answers a question the other platforms leave implicit, with a comparison table right on the page: Unlock removes existing protection using the current password, Protect applies a new one, and the two nodes are described explicitly as opposites meant to be chained in the same workflow, unlock first, process, then re-protect before the file leaves the system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/security/add-password-to-pdf/" rel="noopener noreferrer"&gt;Make's Add Password to PDF&lt;/a&gt; is where the shape changes outright. Instead of one password field, it asks for two: a User Password that gates opening, and a separate Owner Password that gates permissions, the classic two-password model the PDF specification itself supports but that REST's single &lt;code&gt;password&lt;/code&gt; field never surfaces. Make's own permission enum matches REST's eight values exactly, so the divergence here is entirely in the password structure. The unlock side carries its own naming quirk too: Make does not call it "Unlock," it calls it &lt;a href="https://docs.pdf4me.com/integration/make/security/remove-password-from-pdf/" rel="noopener noreferrer"&gt;Remove Password from PDF&lt;/a&gt;, and its own documentation is explicit that a single correct password, user or owner, clears both restriction types in one pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/security/protect-pdf/" rel="noopener noreferrer"&gt;Zapier's Protect PDF&lt;/a&gt; also splits into a User Password and an Owner Password, matching Make's structure. But it drops the permission enum entirely: there is no field to choose which specific actions get blocked. The Owner Password alone gates an unlabeled bundle of restrictions, with no way to select &lt;code&gt;Fill Forms&lt;/code&gt; versus &lt;code&gt;Copy&lt;/code&gt; the way REST, Power Automate, Make, and n8n all allow. &lt;a href="https://docs.pdf4me.com/integration/zapier/security/unlock-pdf/" rel="noopener noreferrer"&gt;Unlock PDF in Zapier&lt;/a&gt; needs only the file and the exact password, and its documentation makes the same point n8n's does: this removes a known password, it does not crack one.&lt;/p&gt;

&lt;p&gt;Line the five surfaces up on two dimensions and no pair matches completely. REST, Power Automate, and n8n use one password field. Make and Zapier use two. REST's permission enum has eight values. Make matches it exactly. Power Automate adds a ninth. n8n renames and reduces to seven. Zapier has none at all. The underlying encryption is the same operation in every case. The interface a developer has to code against is not, and nothing in any single platform's own documentation says so, because each page only ever describes itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually matters when building against this
&lt;/h2&gt;

&lt;p&gt;None of this is a documentation failure so much as five teams building the interface that felt most natural for their own platform's conventions, and none of it should be surprising once it is written down in one place. In practice: read the specific platform's own parameter table before assuming a REST integration guide transfers directly, because both the password structure and the permission vocabulary change underneath a feature name that stays constant. On any platform that does expose a permission enum, treat it as a full allow-list, not a place to name the one thing that should be blocked. If a form only needs &lt;code&gt;Fill Forms&lt;/code&gt; access, decide up front whether print and copy access should genuinely disappear too, because that is what happens by default. The &lt;a href="https://docs.pdf4me.com/url-api-tester/protect-document/" rel="noopener noreferrer"&gt;API Tester for Protect Document&lt;/a&gt; is the fastest way to see this directly: pick one permission value, send the request, and open the result to see exactly what stayed available and what silently did not.&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>security</category>
      <category>automation</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Single and Multiple Document Generation Aren't the Same Endpoint With an Array Bolted On</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 09 Sep 2026 12:58:59 +0000</pubDate>
      <link>https://dev.to/pdf4me/single-and-multiple-document-generation-arent-the-same-endpoint-with-an-array-bolted-on-9na</link>
      <guid>https://dev.to/pdf4me/single-and-multiple-document-generation-arent-the-same-endpoint-with-an-array-bolted-on-9na</guid>
      <description>&lt;p&gt;A team that has already wired up &lt;a href="https://docs.pdf4me.com/pdf4me-api/generate/generate-document-single/" rel="noopener noreferrer"&gt;Generate Document (Single)&lt;/a&gt; will reasonably assume batch generation is the same call with an array bolted onto the data field. It is not. Generate Documents (Multiple) is a separate endpoint, with a narrower set of accepted inputs, one output format Single cannot touch at all, and a response envelope shaped nothing like the one Single returns. This post covers the exact differences, with live-verified request and response shapes for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint path trap
&lt;/h2&gt;

&lt;p&gt;Single lives at &lt;code&gt;POST /api/v2/GenerateDocumentSingle&lt;/code&gt;. The batch version, despite its own docs page being titled "Generate Documents (Multiple)," lives at &lt;code&gt;POST /api/v2/GenerateDocumentMultiple&lt;/code&gt;, singular Document, no trailing s. The &lt;a href="https://docs.pdf4me.com/pdf4me-api/generate/generate-documents-multiple/" rel="noopener noreferrer"&gt;Generate Documents (Multiple)&lt;/a&gt; docs page calls this out directly as a common mistake: the plural path, &lt;code&gt;GenerateDocumentsMultiple&lt;/code&gt;, 404s. Guessing at the plural because Single's own name pattern suggests it is the fastest way to fail a request before it reaches the template engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Correct&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.pdf4me.com/api/v2/GenerateDocumentMultiple &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; @payload.json# 404s, easy to &lt;span class="nb"&gt;type &lt;/span&gt;by habit from the Single endpoint&lt;span class="s1"&gt;'s name
curl -X POST https://api.pdf4me.com/api/v2/GenerateDocumentsMultiple ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Template and data types are not the same set
&lt;/h2&gt;

&lt;p&gt;Single accepts five &lt;code&gt;templateFileType&lt;/code&gt; values: &lt;code&gt;Docx&lt;/code&gt;, &lt;code&gt;MailMerge&lt;/code&gt;, &lt;code&gt;GoogleDocs&lt;/code&gt;, &lt;code&gt;HTML&lt;/code&gt;, and &lt;code&gt;PDF&lt;/code&gt;. Docx, MailMerge, and GoogleDocs templates can render to &lt;code&gt;PDF&lt;/code&gt; or &lt;code&gt;Docx&lt;/code&gt;. HTML templates render to &lt;code&gt;HTML&lt;/code&gt; only. PDF templates render to &lt;code&gt;PDF&lt;/code&gt; only, and casing matters throughout the API.&lt;/p&gt;

&lt;p&gt;Multiple cuts that list to three: &lt;code&gt;Docx&lt;/code&gt;, &lt;code&gt;HTML&lt;/code&gt;, and &lt;code&gt;PDF&lt;/code&gt;. MailMerge and GoogleDocs, both fully supported on Single, are not options on Multiple at all. A mail-merge Word template that renders fine one record at a time through Single gets rejected outright the moment it is pointed at Multiple for a batch run.&lt;/p&gt;

&lt;p&gt;The data side narrows the same way. Single accepts &lt;code&gt;documentDataType&lt;/code&gt; values of &lt;code&gt;Json&lt;/code&gt;, &lt;code&gt;XML&lt;/code&gt;, or &lt;code&gt;Csv&lt;/code&gt;, with exactly one of &lt;code&gt;documentDataText&lt;/code&gt; (inline text) or &lt;code&gt;documentDataFile&lt;/code&gt; (Base64 or a URL) set, never both. Multiple accepts only &lt;code&gt;Json&lt;/code&gt; or &lt;code&gt;XML&lt;/code&gt;. Csv, the format a spreadsheet-driven workflow reaches for first, does not exist on the batch endpoint.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Generate Document (Single)&lt;/th&gt;
&lt;th&gt;Generate Documents (Multiple)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v2/GenerateDocumentSingle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v2/GenerateDocumentMultiple&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templateFileType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docx, MailMerge, GoogleDocs, HTML, PDF&lt;/td&gt;
&lt;td&gt;Docx, HTML, PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;documentDataType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Json, XML, Csv&lt;/td&gt;
&lt;td&gt;Json, XML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;outputType&lt;/code&gt; extras&lt;/td&gt;
&lt;td&gt;none beyond PDF/Docx/HTML&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xlsx&lt;/code&gt; (Docx or PDF templates only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response&lt;/td&gt;
&lt;td&gt;raw file binary (200) or poll URL (202)&lt;/td&gt;
&lt;td&gt;JSON &lt;code&gt;outputDocuments[]&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where Multiple pulls ahead
&lt;/h2&gt;

&lt;p&gt;There is exactly one place the trade runs the other way. Multiple's &lt;code&gt;outputType&lt;/code&gt; field accepts &lt;code&gt;xlsx&lt;/code&gt; for Docx or PDF templates, alongside the usual PDF and Docx choices. Single has no equivalent option under any template type. A workflow that needs to turn a batch of records into a formatted Excel workbook, rather than a folder full of individual PDFs or Word files, has to go through Multiple to get there, even if every other part of the job looks like a single-document task.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response shape stops resembling anything
&lt;/h2&gt;

&lt;p&gt;Single's synchronous response, on a 200, is the rendered file itself, delivered as binary content with the appropriate content type. On a 202, a &lt;code&gt;Location&lt;/code&gt; header points at a poll URL that eventually resolves to that same binary file. There is no wrapper JSON to parse on the happy path.&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="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;templateFileType&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;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;templateFileName&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-template.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;templateFileData&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;&amp;lt;base64 template&amp;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;documentDataType&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;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;documentDataText&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;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoiceNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;outputType&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;PDF&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/GenerateDocumentSingle&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;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;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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple's response is a JSON object built around an &lt;code&gt;outputDocuments&lt;/code&gt; array, one entry per generated file. Here the docs page adds a detail worth building defensively around from the start: each entry is expected to carry a &lt;code&gt;fileName&lt;/code&gt; and a Base64 &lt;code&gt;streamFile&lt;/code&gt;, but the page itself notes the field may also appear as &lt;code&gt;fileContent&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, or &lt;code&gt;data&lt;/code&gt; instead. That is PDF4me's own written acknowledgment that the per-file field name in this response is not fixed across every call.&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;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;templateFileType&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;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;templateFileName&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-template.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;templateFileData&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;&amp;lt;base64 template&amp;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;documentDataType&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;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;documentDataText&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;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoiceNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}, {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Globex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoiceNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-1043&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;outputType&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;PDF&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/GenerateDocumentMultiple&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;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;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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputDocuments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;file_bytes&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;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;doc&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;streamFile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;doc&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;fileContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;doc&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;doc&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="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;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fileName&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;file_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code that decodes &lt;code&gt;streamFile&lt;/code&gt; and assumes that key will always be present is one field-name change away from silently failing to extract a single document out of the batch, even though the request itself succeeded and the response came back with a 200.&lt;/p&gt;

&lt;p&gt;Both endpoints share the same asynchronous pattern underneath these differences. Sending &lt;code&gt;IsAsync: true&lt;/code&gt; can return a 202 with a &lt;code&gt;Location&lt;/code&gt; header instead of the finished result, and the caller polls that URL with the same Authorization header until it resolves. That part of the contract is consistent. What is not consistent is everything about what gets accepted going in and what comes back going out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Template authoring is common ground either way
&lt;/h2&gt;

&lt;p&gt;Regardless of which endpoint a workflow ends up using, both render against the same mustache-style placeholder engine. The &lt;a href="https://docs.pdf4me.com/general-guidelines/word-template-syntax/overview/" rel="noopener noreferrer"&gt;Word Template Syntax Overview&lt;/a&gt; covers that engine, and the &lt;a href="https://docs.pdf4me.com/general-guidelines/word-template-variables/" rel="noopener noreferrer"&gt;Variables in Templates&lt;/a&gt; guide documents the &lt;code&gt;{{fieldName}}&lt;/code&gt; binding syntax that maps a JSON key onto a spot in the Word document. For anything beyond flat fields, the &lt;a href="https://docs.pdf4me.com/general-guidelines/word-template-tables/" rel="noopener noreferrer"&gt;Tables in Templates&lt;/a&gt; guide explains the row-repeat syntax that turns a JSON array into a dynamic table, which matters most on Multiple, where the whole point of the call is rendering once per array element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every integration platform keeps the split intact
&lt;/h2&gt;

&lt;p&gt;None of PDF4me's no-code integrations collapse Single and Multiple into one action. Power Automate offers &lt;a href="https://docs.pdf4me.com/integration/power-automate/generate/generate-document-single/" rel="noopener noreferrer"&gt;Single&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/power-automate/generate/generate-documents-multiple/" rel="noopener noreferrer"&gt;Multiple&lt;/a&gt; as distinct actions. Make does the same with its own &lt;a href="https://docs.pdf4me.com/integration/make/generate/generate-document-single/" rel="noopener noreferrer"&gt;Single&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/make/generate/generate-documents-multiple/" rel="noopener noreferrer"&gt;Multiple&lt;/a&gt; modules, as does Zapier with &lt;a href="https://docs.pdf4me.com/integration/zapier/generate/generate-document-single/" rel="noopener noreferrer"&gt;Single&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/zapier/generate/generate-documents-multiple/" rel="noopener noreferrer"&gt;Multiple&lt;/a&gt;, and n8n with its &lt;a href="https://docs.pdf4me.com/integration/n8n/generate/generate-document-single/" rel="noopener noreferrer"&gt;Single&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/generate/generate-documents-multiple/" rel="noopener noreferrer"&gt;Multiple&lt;/a&gt; nodes. Choosing the wrong one in any of these builders means hitting the same template-type and data-type restrictions described above, just surfaced through a no-code form instead of a raw request body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it before wiring it in
&lt;/h2&gt;

&lt;p&gt;PDF4me's interactive &lt;a href="https://docs.pdf4me.com/url-api-tester/generate-document-single/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; covers Single, and a &lt;a href="https://docs.pdf4me.com/url-api-tester/generate-documents-multiple/" rel="noopener noreferrer"&gt;separate API Tester page&lt;/a&gt; covers Multiple, letting a real request run against both endpoints with the same template and comparable data. Running that comparison once, before either endpoint gets wired into production code, is a cheaper way to learn the contract than discovering it from a parser that expected a file and got a JSON object holding a field it did not know to look for.&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>documentgeneration</category>
    </item>
    <item>
      <title>Extract Worksheets Doesn't Skip Sheets When You Send No Filter, It Extracts All of Them</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:40:11 +0000</pubDate>
      <link>https://dev.to/pdf4me/extract-worksheets-doesnt-skip-sheets-when-you-send-no-filter-it-extracts-all-of-them-252g</link>
      <guid>https://dev.to/pdf4me/extract-worksheets-doesnt-skip-sheets-when-you-send-no-filter-it-extracts-all-of-them-252g</guid>
      <description>&lt;p&gt;Send an Excel Extract Worksheets request with no worksheet names and no worksheet indexes, and the response comes back with every sheet in the workbook, not the first one, not an empty result, not an error. That is documented behavior, not a fallback for a missing parameter. A team building an export step that runs Extract Worksheets on a customer-uploaded workbook, expecting to pull just the tab their template looks for, can end up shipping every internal tab in that workbook downstream instead, simply because the filter fields were left blank during testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Worksheets: union selection, empty means everything
&lt;/h2&gt;

&lt;p&gt;PDF4me's Extract Worksheets endpoint (&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-worksheets/" rel="noopener noreferrer"&gt;https://docs.pdf4me.com/pdf4me-api/excel/extract-worksheets/&lt;/a&gt;) calls this out in its own Important Facts section: names and indexes combine as a union, and leaving both empty extracts everything. The request body looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;office/ApiV&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;Excel/ExcelExtractWorksheet&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;"document"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"report.xlsx"&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;"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 workbook bytes&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;"extractWorksheetToExcelAction"&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;"worksheetNames"&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;"worksheetIndexes"&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;"cultureName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&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;With both arrays empty, every worksheet comes back keyed by name in the response. If worksheetNames is ["Summary"] and worksheetIndexes is [0], and index 0 happens to be a different sheet than Summary, the response contains both sheets, not the intersection. Name matching is case-sensitive, and because a match failure is silent rather than an outright error unless nothing at all matches, a mistyped case can quietly drop a sheet out of a multi-sheet extraction without surfacing a warning anywhere in the response.&lt;/p&gt;

&lt;p&gt;The same union-and-empty-means-all logic holds on every platform PDF4me ships this feature to. &lt;a href="https://docs.pdf4me.com/integration/make/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Make's Extract Worksheets&lt;/a&gt; module and &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Zapier's Extract Worksheets&lt;/a&gt; action both document the identical union behavior, and so do &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Power Automate's Extract Worksheets&lt;/a&gt; action and the &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/extract-worksheets/" rel="noopener noreferrer"&gt;n8n Extract Worksheets&lt;/a&gt; node. Where they stop agreeing is on a detail a developer would reasonably assume travels unchanged across one vendor's own integrations: what number identifies the first worksheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worksheet index that changes meaning by platform
&lt;/h2&gt;

&lt;p&gt;On the REST API, worksheet indexes are 0-based. Index 0 is the first sheet. Power Automate keeps that convention exactly, its documentation stating plainly that index 0 is the first worksheet and index 1 the second, matching the API underneath it. The n8n node does the same, with its own parameter table marking Worksheet Indexes as 0-based. Two platforms, one convention.&lt;/p&gt;

&lt;p&gt;Make and Zapier both break from that. In Make's Extract Worksheets module, the same Worksheet Indexes field is explicitly 1-based, with the documentation noting that index 1 means the first worksheet. Zapier goes further and flags the mismatch itself: its own Extract Worksheets documentation states that indexes are 1-based and adds that this is different from the zero-based indexing used in many programming languages, which is Zapier telling its own users, in its own docs, that the convention here is not the one they are used to. Index 2 means the third worksheet in one platform and the second in another for the exact same PDF4me operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Rows: a JSON-out endpoint, not a file-out one
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-rows/" rel="noopener noreferrer"&gt;Extract Rows&lt;/a&gt; is a different endpoint from Extract Worksheets, built to pull a row and column range back as JSON rather than hand back a file. Its own REST documentation is explicit that document and fileName come back null by design, because the point of this call is structured data, not a document to pass downstream.&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="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;office/ApiV&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;Excel/ExcelExtractRows&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;"document"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"report.xlsx"&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;"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 workbook bytes&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;"extractRowsToExcelAction"&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;"worksheetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sheet1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fromRow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"toRow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cultureName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint also carries options worth knowing before wiring it into a pipeline: Has Header Row controls whether the first row extracted becomes the JSON keys or gets treated as data, Exclude Hidden Rows and Exclude Hidden Columns can filter out anything hidden in the source sheet, and Export Values As Text forces every cell to a string instead of preserving numbers, dates, and booleans in their native JSON types. Row and column indexes here are 0-based on REST, and that convention carries cleanly through Power Automate and n8n as well: the &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/extract-rows/" rel="noopener noreferrer"&gt;Power Automate Extract Rows&lt;/a&gt; action documents First Row and First Column as 0-based, and the &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/extract-rows/" rel="noopener noreferrer"&gt;n8n Extract Rows&lt;/a&gt; node matches it, down to the same rule that -1 means read to the last row with data.&lt;/p&gt;

&lt;p&gt;Here is where the two platforms that broke the worksheet-index convention diverge from each other as well. &lt;a href="https://docs.pdf4me.com/integration/make/excel/extract-rows/" rel="noopener noreferrer"&gt;Make's Extract Rows&lt;/a&gt; module keeps First Row, Last Row, First Column, and Last Column all 0-based, the same as REST, even though Make's own Extract Worksheets module one page over uses 1-based worksheet indexes. Inside a single platform, one Excel action counts from zero and the neighboring one counts from one. Zapier does not split the difference the way Make does. Its &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/extract-rows/" rel="noopener noreferrer"&gt;Extract Rows&lt;/a&gt; action is 1-based for rows and columns as well as worksheets, and once again the documentation names the gap itself, noting that row 1 in the request means Excel row 1, aimed squarely at developers used to counting from zero. Of the four integration platforms, Zapier is the only one where every index in both Excel actions has shifted by one relative to the REST API underneath it.&lt;/p&gt;

&lt;p&gt;None of this makes any single convention wrong. Row 1 meaning the first row is arguably more intuitive to a spreadsheet user than row 0 meaning the first row, and it would not be surprising if that intuition is exactly why two no-code platforms built for less technical users chose to count from one somewhere along the way. But intuitive and consistent are different qualities, and a team running the same read-a-range logic across two of these platforms is copying numbers that mean different things depending on where they land.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge Files: order comes from SortPosition, not array order
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/merge-files/" rel="noopener noreferrer"&gt;Merge Files&lt;/a&gt; sidesteps the indexing question entirely since it does not read by position, but it has its own ordering rule worth knowing before relying on it.&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="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;office/ApiV&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;Excel/ExcelMergeFiles&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;"mergeFilesToExcelAction"&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;"documents"&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;"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;"north.xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileContent"&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&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;"sortPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="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;"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;"south.xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileContent"&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&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;"sortPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;"outputFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"combined"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cultureName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Documents array does not merge in the order the array was written. It merges by SortPosition, ascending, so a request that lists file C first but assigns it SortPosition 3, while file A carries SortPosition 1, places A first in the output regardless of array order. &lt;a href="https://docs.pdf4me.com/integration/make/excel/merge-files/" rel="noopener noreferrer"&gt;Make's Merge Files module&lt;/a&gt; and the &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/merge-files/" rel="noopener noreferrer"&gt;n8n Merge Files node&lt;/a&gt; both carry the same Sort Position field forward, and &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/merge-files/" rel="noopener noreferrer"&gt;Power Automate's Merge Files action&lt;/a&gt; documents identical behavior through its own MergeDocument objects. &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/merge-files/" rel="noopener noreferrer"&gt;Zapier's Merge Files action&lt;/a&gt; uses Sort Position the same way, and its documentation adds a detail worth flagging on its own: the output field shape for Merge Files is explicitly shared across Merge Files, Add Rows, Delete Rows, and other PDF4me Excel actions in Zapier, so code parsing that response should not assume every field is unique to this one operation.&lt;/p&gt;

&lt;p&gt;Across every platform, worksheet name collisions during a merge get resolved automatically by suffixing duplicates rather than failing the request, and CSV is the one output format that only carries the first worksheet forward, since a CSV file cannot represent more than one sheet at a time. A monthly consolidation flow that merges five regional workbooks and outputs CSV for a downstream database load will silently lose four sheets worth of data unless XLSX or XLS is chosen instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the index before you trust it
&lt;/h2&gt;

&lt;p&gt;The practical habit this points to is simple, and it costs one test run to build. Do not assume an index value is portable between two PDF4me integrations just because it is the same feature from the same vendor. Before wiring a row or worksheet number into a production flow, run the same request once against a known workbook with index 0 and again with index 1, note which sheet or row actually comes back on that specific platform, and treat the REST API's 0-based convention as a baseline that Power Automate and n8n happen to preserve rather than a rule every PDF4me integration follows.&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>excel</category>
      <category>api</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
