<?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>Draft vs High: The OCR Quality Parameter Nobody Reads the Docs For</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 12 Aug 2026 04:57:41 +0000</pubDate>
      <link>https://dev.to/pdf4me/draft-vs-high-the-ocr-quality-parameter-nobody-reads-the-docs-for-199f</link>
      <guid>https://dev.to/pdf4me/draft-vs-high-the-ocr-quality-parameter-nobody-reads-the-docs-for-199f</guid>
      <description>&lt;p&gt;Five different places document PDF4me's OCR quality setting: the REST API and four no-code platforms. Read all five pages back to back and you'll find they're not even using the same two words for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the quality setting is actually deciding
&lt;/h2&gt;

&lt;p&gt;A scanned page, or a PDF made from a photo of a document, is just an image. There's no text underneath it for a computer to read, search, or copy. OCR is the step that looks at each page image, works out what characters are on it, and writes that back into the file as real, selectable text.&lt;/p&gt;

&lt;p&gt;The quality setting decides how hard that recognition pass works. A born-digital PDF that already has most of its text as a real text layer only needs a light, single pass to catch anything missing. A fully scanned or photographed document needs every page individually recognized, since none of it is real text to begin with. Pick the light option on a scan and you get an empty or garbled result. Pick the heavy option on a file that's already text and it runs the recognition anyway, on pages that didn't need it.&lt;/p&gt;

&lt;p&gt;On PDF4me's own &lt;a href="https://docs.pdf4me.com/pdf4me-api/find-search/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;, this is a required field in the request body: &lt;code&gt;POST /api/v2/ConvertOcrPdf&lt;/code&gt; with &lt;code&gt;qualityType&lt;/code&gt; set to the literal string &lt;code&gt;"Draft"&lt;/code&gt; or &lt;code&gt;"High"&lt;/code&gt;. Draft is a fast single pass suited to normal PDFs that mostly already have a text layer. High is full per-page recognition, built for scanned or image-based documents. Pair it with &lt;code&gt;ocrWhenNeeded&lt;/code&gt; (also a string, &lt;code&gt;"true"&lt;/code&gt; or &lt;code&gt;"false"&lt;/code&gt;) to skip any page that's already searchable instead of reprocessing it, and &lt;code&gt;language&lt;/code&gt; when the recognized text comes back garbled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five surfaces, two different vocabularies
&lt;/h2&gt;

&lt;p&gt;Here's where it gets genuinely confusing, and it's the actual reason this parameter gets misconfigured more often than it should: PDF4me's own &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/pdf-ocr/" rel="noopener noreferrer"&gt;Power Automate PDF OCR action&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/find-search/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;n8n's Convert PDF to Editable PDF Using OCR node&lt;/a&gt; both use the exact same words as the REST API: Quality Type, set to Draft or High. Power Automate's docs even note the default is Draft if you don't specify one.&lt;/p&gt;

&lt;p&gt;But &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/create-searchable-pdf/" rel="noopener noreferrer"&gt;Zapier's PDF OCR action&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/make/find-search/pdf-ocr/" rel="noopener noreferrer"&gt;Make's PDF OCR module&lt;/a&gt;, built on the exact same underlying feature, both call the same choice Standard or Expert instead. Standard for normal PDFs, Expert for scanned or photographed documents. Same decision, same two options, different pair of words, on two of the five surfaces that expose it.&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;Parameter name&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Docs page&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;&lt;code&gt;qualityType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Draft / High&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/find-search/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;ConvertOcrPdf&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power Automate&lt;/td&gt;
&lt;td&gt;Quality Type&lt;/td&gt;
&lt;td&gt;Draft / High (default: Draft)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/pdf-ocr/" rel="noopener noreferrer"&gt;PDF OCR action&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;td&gt;Quality Type&lt;/td&gt;
&lt;td&gt;Draft / High (response schema also lists "Archival")&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/find-search/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;Convert PDF to Editable PDF Using OCR&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier&lt;/td&gt;
&lt;td&gt;Quality Type&lt;/td&gt;
&lt;td&gt;Standard / Expert&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/create-searchable-pdf/" rel="noopener noreferrer"&gt;PDF OCR - Searchable Document&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make&lt;/td&gt;
&lt;td&gt;Quality Type&lt;/td&gt;
&lt;td&gt;Standard / Expert&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.pdf4me.com/integration/make/find-search/pdf-ocr/" rel="noopener noreferrer"&gt;PDF OCR module&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So if you've automated this feature on more than one platform, there's a real chance you've been reading two completely different vocabularies for the same setting without realizing they map onto each other. A team that builds the Power Automate flow and hands the Zapier version to someone else, or vice versa, can easily lose that connection entirely.&lt;/p&gt;

&lt;p&gt;There's a third wrinkle worth knowing about if you're on n8n specifically: its own documented output schema lists the possible values that come back in the response as "Draft", "High", and Archival, even though the parameter description itself only mentions Draft and High as the choices you can set. Whether Archival is a real third quality tier or just a stray value from a shared internal schema isn't something the parameter table itself explains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other setting worth knowing about: OCR Only When Needed
&lt;/h2&gt;

&lt;p&gt;Every one of these five surfaces also exposes a second, related toggle: &lt;code&gt;ocrWhenNeeded&lt;/code&gt; on the REST API, OCR Only When Needed on Power Automate and n8n. Set it on and the engine checks each page before processing, skipping any page that already has selectable text and only running recognition on the pages that are actually images. This matters most on hybrid documents: a contract that was born digital, printed, signed by hand, and rescanned. Most of that file is still real text. Only the signature page, or a handwritten margin note, became image data. Turning this on means you don't have to choose one setting for the whole document, the engine makes that call per page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The language parameter isn't formatted the same way twice either
&lt;/h2&gt;

&lt;p&gt;If the recognized text comes back garbled, every one of these five surfaces lets you tell the engine what language to expect. What they don't agree on is the format. The REST API's own sample payload and n8n's parameter example both use the full language name, &lt;code&gt;"English"&lt;/code&gt;. Power Automate's example uses a two-letter code, &lt;code&gt;en&lt;/code&gt;. Make's parameter table lists three-letter codes: &lt;code&gt;eng&lt;/code&gt;, &lt;code&gt;deu&lt;/code&gt;, &lt;code&gt;fra&lt;/code&gt;, &lt;code&gt;spa&lt;/code&gt;, &lt;code&gt;ita&lt;/code&gt;, &lt;code&gt;por&lt;/code&gt;. Copy a value from one platform's documentation into another platform's field and there's a real chance it silently fails to match, or falls back to auto-detection instead of doing what you asked. Always check the example value on the specific page you're building against, not the platform you happen to remember from last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling it directly: a live-verified request
&lt;/h2&gt;

&lt;p&gt;Here's the exact request shape, straight from PDF4me's own docs page for &lt;code&gt;ConvertOcrPdf&lt;/code&gt;, adapted into Python with &lt;code&gt;requests&lt;/code&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;your-pdf4me-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ConvertOcrPdf&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;scanned-contract.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scanned-contract.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualityType&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;High&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;ocrWhenNeeded&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;true&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;language&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;English&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;outputFormat&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;true&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="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;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;searchable.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;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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Large scan: poll the Location URL with the same Authorization header
&lt;/span&gt;    &lt;span class="n"&gt;poll_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="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;Processing async, poll: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;poll_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out about this payload, all live-verified against the current docs page rather than assumed: &lt;code&gt;ocrWhenNeeded&lt;/code&gt; and &lt;code&gt;outputFormat&lt;/code&gt; are sent as the &lt;em&gt;strings&lt;/em&gt; &lt;code&gt;"true"&lt;/code&gt;/&lt;code&gt;"false"&lt;/code&gt;, not JSON booleans, while &lt;code&gt;isAsync&lt;/code&gt; is a real boolean. Mixing those two types up is a documented, common cause of 400 errors. &lt;code&gt;outputFormat&lt;/code&gt; is required, but the docs page itself doesn't explain what it controls beyond noting it's sent as &lt;code&gt;"true"&lt;/code&gt; in the official samples, worth flagging rather than guessing at. The official samples also include a &lt;code&gt;mergeAllSheets&lt;/code&gt; boolean field on this endpoint, whose purpose isn't clearly documented for a PDF-in, PDF-out OCR action either, exactly the kind of parameter that's easy to skip past without reading.&lt;/p&gt;

&lt;p&gt;For Python, C#, Java, JavaScript, and Salesforce reference implementations, PDF4me publishes official samples alongside the docs page itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for the heavier setting even on a file that looks fine
&lt;/h2&gt;

&lt;p&gt;The trap isn't obviously scanned documents, most people get those right by instinct. It's the hybrid case above: a file that looks like a normal PDF at a glance but has had part of its content replaced by a scanned or photographed page at some point. If a document has passed through a printer, a scanner, a fax machine, or a phone camera at any point in its life, even once, treat it as a High (or Expert) candidate. If it has only ever existed as a digital file, the lighter setting gives you the same result while skipping the recognition step entirely. When your pipeline handles a mixed batch of both, defaulting to the heavier setting is the safer call, since running it on a file that doesn't need it doesn't change the outcome, it just also performs the recognition pass on a file that didn't need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist worth keeping next to your workflow
&lt;/h2&gt;

&lt;p&gt;Before wiring up OCR in any of these five places, it's worth asking one question: has this file, or any part of it, ever touched a printer, a scanner, a fax machine, or a camera? If the answer is yes, or even maybe, reach for the heavier quality setting, whichever pair of words your platform happens to use for it. Turn on the skip-already-searchable-pages toggle so the engine isn't reprocessing text that's already there. And if the output looks garbled afterward, check the language parameter, and double-check the exact format that specific platform's own docs page expects, before assuming the OCR itself failed.&lt;/p&gt;

&lt;p&gt;Whether you're calling the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;REST API directly&lt;/a&gt;, or building on &lt;a href="https://docs.pdf4me.com/integration/make/find-search/pdf-ocr/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/pdf-ocr/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/create-searchable-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, or &lt;a href="https://docs.pdf4me.com/integration/n8n/find-search/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, you can also test the REST endpoint interactively, with a real file, through the &lt;a href="https://docs.pdf4me.com/url-api-tester/convert-pdf-to-editable-pdf-using-ocr/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; before wiring it into anything.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>pdf</category>
      <category>ocr</category>
      <category>automation</category>
    </item>
    <item>
      <title>Blank Pages Are a Data Quality Problem Before They're a Design Problem</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:13:07 +0000</pubDate>
      <link>https://dev.to/pdf4me/blank-pages-are-a-data-quality-problem-before-theyre-a-design-problem-n12</link>
      <guid>https://dev.to/pdf4me/blank-pages-are-a-data-quality-problem-before-theyre-a-design-problem-n12</guid>
      <description>&lt;p&gt;A scanner inserts a blank sheet between every duplex-scanned document in a batch. A fax gateway drops a blank cover page in front of every inbound PDF. A mail merge leaves a trailing empty page at the end of a contract because the last section happened to end exactly on a page break. None of that is a formatting problem. It's a data quality problem, because every automated step downstream of that PDF treats a page as a unit of meaning, and a blank page is a unit of nothing.&lt;/p&gt;

&lt;p&gt;Run OCR across it and you pay for a page with no text to extract. Run an AI parser across it and you get a document object with empty fields, or worse, a parser that silently treats the blank page as the start of a new record. Split the file by page count or route it by page index and the blank page throws off every number after it. None of these are edge cases. They're the default outcome of feeding real-world scanned or merged PDFs into a pipeline that assumes every page carries content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just skip it visually" doesn't scale
&lt;/h2&gt;

&lt;p&gt;At one document, a blank page is nothing, you glance at it, you move on. At the batch sizes automation actually exists for, hundreds of scanned intake forms, thousands of merged invoices, that glance becomes a full-time job nobody has. The fix isn't a person paging through PDFs looking for empty sheets. It's a step in the pipeline that detects and removes them before anything else runs.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/organize/delete-blank-pages-from-pdf/" rel="noopener noreferrer"&gt;Delete Blank Pages&lt;/a&gt; REST endpoint (&lt;code&gt;POST /api/v2/DeleteBlankPages&lt;/code&gt;) does exactly that: it scans a PDF, identifies pages that match your definition of blank, and returns the file with those pages removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Blank" is a choice, not a single definition
&lt;/h2&gt;

&lt;p&gt;A page that's truly, completely empty, no text, no image, nothing, is the easy case. The harder case, and the far more common one in real scanned batches, is a page that carries &lt;em&gt;something&lt;/em&gt;: a scanned photo or logo with no text on it, or a page of body text with no image. Whether either of those counts as "blank" depends entirely on what your pipeline actually needs gone.&lt;/p&gt;

&lt;p&gt;That's what the endpoint's &lt;code&gt;deletePageOption&lt;/code&gt; parameter controls, a string set to one of three values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NoTextNoImages&lt;/code&gt; removes only pages with neither text nor an image present, the strictest, most conservative definition of blank.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NoText&lt;/code&gt; removes any page with no text content, even if it has an image on it, useful when a pipeline only cares about pages with readable text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NoImages&lt;/code&gt; removes any page with no image, even if it has text, the mirror case for a pipeline that only cares about pages carrying visual content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is more correct than the others. The right one depends on which kind of near-empty page your specific documents produce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this belongs in a pipeline, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Blank-page removal is a cleanup step, not an extraction step, which means it belongs early: before OCR runs, before an AI parser sees the file, before anything splits the PDF by page count, page index, or a repeating text or barcode marker. Every one of those downstream steps works better, and cheaper, on a PDF where every remaining page is actually worth processing.&lt;/p&gt;

&lt;p&gt;It's worth being clear about what this endpoint doesn't do, too. It detects blank pages automatically based on the option you choose, it doesn't remove pages you name explicitly. If you already know page 3 and page 7 need to go, &lt;a href="https://docs.pdf4me.com/pdf4me-api/organize/delete-pages/" rel="noopener noreferrer"&gt;Delete Pages&lt;/a&gt; is the right tool, direct page targeting, not content-based detection. The two solve different problems: one answers "which pages are empty," the other answers "which pages did I already decide to remove."&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling it directly
&lt;/h2&gt;

&lt;p&gt;Here's a working Python example against the raw REST API, adapted from PDF4me's official &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Organize/Delete%20Blank%20Pages%20from%20PDF/Python/Delete%20Blank%20Pages%20from%20PDF" rel="noopener noreferrer"&gt;sample repository&lt;/a&gt;, live-verified against the docs page's own Payload and Parameters sections:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&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;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/DeleteBlankPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;deletePageOption&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;NoTextNoImages&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 "NoText", "NoImages"
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;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;data&lt;/span&gt;&lt;span class="o"&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;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="c1"&gt;# Synchronous success, cleaned PDF returned immediately
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Larger files process asynchronously, poll using the returned jobId
&lt;/span&gt;    &lt;span class="n"&gt;job_id&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;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;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&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;Processing async, job ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth calling out. First, &lt;code&gt;deletePageOption&lt;/code&gt; is required, there's no default, you have to pick one of the three values explicitly every time. Second, don't assume every response is synchronous: larger files can come back as &lt;code&gt;202 Accepted&lt;/code&gt; with a job ID instead of the finished PDF, meaning your integration needs a polling step rather than treating every call as instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it without hand-rolling REST calls
&lt;/h2&gt;

&lt;p&gt;Building directly against the API, the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt; guide covers authentication, the base URL, and the request and response shape every PDF4me endpoint follows, DeleteBlankPages included.&lt;/p&gt;

&lt;p&gt;Most teams aren't writing raw REST calls for this, though, they're wiring it into whichever automation platform already runs their intake pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power Automate&lt;/strong&gt; exposes blank-page removal as a step you can &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf/delete-blank-pages/" rel="noopener noreferrer"&gt;drop directly into a Microsoft 365 flow&lt;/a&gt;: a scanned document lands in a SharePoint library or an email attachment, the flow strips the blank pages, and whatever runs next only ever sees pages with actual content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n&lt;/strong&gt; carries the &lt;a href="https://docs.pdf4me.com/integration/n8n/organize/delete-blank-pages-from-pdf/" rel="noopener noreferrer"&gt;same detection-and-removal logic&lt;/a&gt; into a self-hosted or cloud n8n workflow, useful for teams running document automation on infrastructure they control rather than a third-party SaaS platform.&lt;/p&gt;

&lt;p&gt;Neither Make nor Zapier currently has a dedicated module or action for this specific endpoint. If your pipeline runs on either, the REST API above is the direct path until that changes.&lt;/p&gt;

&lt;p&gt;Before wiring this into any of the above, the &lt;a href="https://docs.pdf4me.com/url-api-tester/delete-blank-pages/" rel="noopener noreferrer"&gt;Delete Blank Pages page in PDF4me's API Tester&lt;/a&gt; lets you upload a real file from your own batch, switch between the three delete options, and see exactly which pages get flagged under each one, interactively, in the browser, with no workflow built yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this needs judgment, not just a default
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NoTextNoImages&lt;/code&gt; is the comprehensive option, and it's tempting to reach for it every time since it sounds like the thorough choice. It isn't always the right one. A page carrying only a faint letterhead image with no text, or only a stamped date with no image, survives &lt;code&gt;NoTextNoImages&lt;/code&gt; untouched because it has &lt;em&gt;something&lt;/em&gt;, even though a human reviewing the batch might call it blank too. Getting the removal you actually want means picking the option that matches what "blank" means for your specific documents, not defaulting to the strictest-sounding name. There's no way to know which option is right without testing against a real sample from your own batch first.&lt;/p&gt;

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

</description>
      <category>pdf</category>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Splitting a PDF by the text on the page, not a barcode you don't have</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:53:25 +0000</pubDate>
      <link>https://dev.to/pdf4me/splitting-a-pdf-by-the-text-on-the-page-not-a-barcode-you-dont-have-5492</link>
      <guid>https://dev.to/pdf4me/splitting-a-pdf-by-the-text-on-the-page-not-a-barcode-you-dont-have-5492</guid>
      <description>&lt;p&gt;Ask a developer to split a batch of merged PDFs and the first idea that comes up is usually a barcode. Print separator sheets, scan them into the stack, split on the barcode. It works, and it's the textbook answer in most document automation tutorials.&lt;/p&gt;

&lt;p&gt;It also assumes something that's often not true: that someone controlled the scanning process in the first place. A lot of merged PDFs never went anywhere near a scanner with separator sheets. They're exports from accounting software, ten invoices concatenated into one file because that's what the export button does. They're patient records pulled from an EMR and stitched together for archival. They're legal bundles where the only structure is a line that says "EXHIBIT" before each new section. There's no barcode to detect, because nobody put one there. What there is, in every one of these cases, is text: a recurring header, a label, a keyword that shows up exactly where a new document starts.&lt;/p&gt;

&lt;p&gt;That's the gap &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-pdf-by-text/" rel="noopener noreferrer"&gt;Split PDF by Text&lt;/a&gt; is built for. Instead of looking for a printed barcode, it searches the actual text content of every page for a string you specify, and treats each matching page as a split boundary. If your merged file has "Invoice Number:" printed near the top of every invoice, or "Patient Name:" at the start of every record, or "--- NEW DOCUMENT ---" inserted as a literal separator line, the engine finds every occurrence and hands back one PDF per section.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening under the hood
&lt;/h2&gt;

&lt;p&gt;The REST endpoint takes a &lt;code&gt;POST&lt;/code&gt; request. You send the source PDF as Base64 in &lt;code&gt;docContent&lt;/code&gt;, an output name in &lt;code&gt;docName&lt;/code&gt;, and the string you want it to search for in &lt;code&gt;text&lt;/code&gt;. The engine runs a full-text search across every page of the document and uses matching pages as split boundaries.&lt;/p&gt;

&lt;p&gt;Here's a minimal Python request against the endpoint, based on the parameter names confirmed in both the live docs page and PDF4me's own &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Merge%20and%20Split/Split%20PDF%20by%20Text" rel="noopener noreferrer"&gt;Split PDF by Text sample repo&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="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;merged_invoices.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;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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice Number:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;splitTextPage&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;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fileNaming&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameAsPerText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/SplitPdfByText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One parameter decides which side of the match the split lands on. &lt;code&gt;splitTextPage&lt;/code&gt; accepts &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;after&lt;/code&gt;, controlling whether the new document starts on the page containing your marker text or the page right after it. A document where "Invoice Number:" sits at the top of each invoice's first page wants &lt;code&gt;before&lt;/code&gt;, so that page stays attached to the invoice it's labeling. A document using a literal separator line like "--- NEW DOCUMENT ---" as a throwaway divider might want &lt;code&gt;after&lt;/code&gt;, so the divider page doesn't end up glued to the following document.&lt;/p&gt;

&lt;p&gt;The other parameter, &lt;code&gt;fileNaming&lt;/code&gt;, decides what the split output files are called. &lt;code&gt;NameAsPerOrder&lt;/code&gt; names them by position, while &lt;code&gt;NameAsPerText&lt;/code&gt; names each output after the matched text itself, useful when that text is something identifying, like an invoice number. For larger files, the API also accepts an &lt;code&gt;async&lt;/code&gt; flag; set it to &lt;code&gt;true&lt;/code&gt; and the request returns a job ID to poll instead of blocking on a synchronous response.&lt;/p&gt;

&lt;p&gt;One thing worth flagging directly: the marketing docs page lists the endpoint path as &lt;code&gt;/api/v2/SplitByText&lt;/code&gt;, but PDF4me's own published sample code, across every language in the samples repo, consistently calls &lt;code&gt;/api/v2/SplitPdfByText&lt;/code&gt; instead. The sample code is what's actually been run against the live API, so that's the path used above. Worth testing against your own account either way before you hardcode anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually pays off, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;The strongest case for text-based splitting is any document that already has a natural label baked into its layout, because someone designed it to be read by a human before you ever thought about automating it. Invoice numbers, patient names, exhibit markers, form titles: these exist because a person needed to identify the page, not because anyone anticipated splitting it programmatically later. That's exactly what makes text splitting reliable here. You're not adding new structure to the document. You're reading structure that was already there.&lt;/p&gt;

&lt;p&gt;Where it gets shakier is anything that started life as a scanned image rather than a digitally generated PDF. Full-text search depends on the PDF actually containing a text layer, not just a picture of text. A scanned page that was never OCR'd has no searchable text for the &lt;code&gt;text&lt;/code&gt; parameter to match against, and no amount of parameter tuning fixes that; the fix is running OCR first, not adjusting this endpoint. It's also worth testing your exact marker string against real documents before trusting it in production, because minor formatting drift, an extra space, a line break in the wrong place, a label that changes wording halfway through a scan batch, is exactly the kind of thing that looks fine in five sample files and quietly fails on file six.&lt;/p&gt;

&lt;p&gt;It's also worth being honest about where this endpoint stops and a different one starts. If your documents already carry printed barcodes as separator markers, &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-by-barcode/" rel="noopener noreferrer"&gt;Split PDF by Barcode&lt;/a&gt; is the more deterministic tool for that job, since it doesn't depend on text formatting at all. And if you're specifically working with Swiss invoicing, &lt;a href="https://docs.pdf4me.com/pdf4me-api/merge-split/split-pdf-by-swiss-qr/" rel="noopener noreferrer"&gt;Split PDF by Swiss QR&lt;/a&gt; is purpose-built to detect the SIX Group Swiss QR bill standard and split batch-printed invoice runs at each QR boundary. These aren't three ways of doing the same thing; they're three answers to three different questions about what marker your documents actually contain. Pick based on what's really in the file, not on which one you've heard of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it before it touches a pipeline
&lt;/h2&gt;

&lt;p&gt;Before wiring this into anything automated, it's worth running a request against a real sample file and actually looking at the response, rather than assuming the parameters behave the way the description implies. The &lt;a href="https://docs.pdf4me.com/url-api-tester/split-pdf-by-text/" rel="noopener noreferrer"&gt;Split PDF by Text API Tester&lt;/a&gt; is built for exactly that: an interactive, in-browser way to send a request and inspect what comes back, including how &lt;code&gt;splitTextPage&lt;/code&gt; behaves on your specific document, before you write a single line of integration code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it plugs in
&lt;/h2&gt;

&lt;p&gt;The REST endpoint is the foundation, but most teams reading this aren't calling it from raw HTTP requests, they're wiring it into whatever automation platform already runs their document workflows. In &lt;a href="https://docs.pdf4me.com/integration/make/merge-split/split-by-text/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, the module drops into a scenario the same way any other PDF4me action does, letting you route each split output straight into the next step, whether that's a folder, a database record, or an email. &lt;a href="https://docs.pdf4me.com/integration/power-automate/merge-split/split-pdf-text/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; exposes the same before/after control across Microsoft 365 workflows, which matters if your merged invoices are already landing in a SharePoint library or a Teams channel. &lt;a href="https://docs.pdf4me.com/integration/zapier/merge-split/split-pdf-by-text/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; covers the same ground for teams running lighter-weight Zaps between apps that were never meant to talk to each other directly. And in &lt;a href="https://docs.pdf4me.com/integration/n8n/merge-split/split-pdf-by-text/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, the node fits into a self-hosted workflow the same way, useful if you're keeping document processing inside infrastructure you control rather than a third-party SaaS queue.&lt;/p&gt;

&lt;p&gt;None of these change what the underlying engine does. They change where the decision to split gets made, and what happens automatically to each resulting file once it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual test
&lt;/h2&gt;

&lt;p&gt;If you're staring at a folder of merged PDFs trying to decide whether text splitting is the right tool, the test isn't "does this file have a barcode." It's simpler: open one of the files and look for a line of text that shows up, consistently, at the start of every section you want split out. If that line exists, a full-text search will find it forty times as reliably as it finds it once. If it doesn't exist, no amount of parameter tuning invents structure that was never in the document to begin with, and that's worth knowing before you build a pipeline around it rather than after.&lt;/p&gt;

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

</description>
      <category>pdf</category>
      <category>automation</category>
      <category>api</category>
      <category>productivity</category>
    </item>
    <item>
      <title>HTML Headers and Footers: Styling PDF Margins With CSS Instead of a PDF Library</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Sat, 08 Aug 2026 06:15:14 +0000</pubDate>
      <link>https://dev.to/pdf4me/html-headers-and-footers-styling-pdf-margins-with-css-instead-of-a-pdf-library-5hma</link>
      <guid>https://dev.to/pdf4me/html-headers-and-footers-styling-pdf-margins-with-css-instead-of-a-pdf-library-5hma</guid>
      <description>&lt;p&gt;The instinct, the moment a ticket says "add our logo and a confidentiality line to every page," is to reach for a PDF manipulation library. Pull in iText, or PDFBox, or pdf-lib. Calculate x and y coordinates by hand. Draw text at a fixed position, in a fixed font, at a fixed size, and hope the next design change does not mean recalculating every coordinate again. That instinct is a leftover from a world where the only way to put content in a PDF's margin was to draw it there, pixel by pixel, in a language built for placing shapes on a page, not for laying out a header.&lt;/p&gt;

&lt;p&gt;The header and footer area of a PDF is margin space, and margin space is exactly what CSS was built to lay out. The alternative to a drawing API is not a bigger drawing API. It is not touching page coordinates at all.&lt;/p&gt;

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

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-header-footer-to-pdf/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF&lt;/a&gt; endpoint is a single &lt;code&gt;POST /api/v2/AddHtmlHeaderFooter&lt;/code&gt; call. Four fields are required: &lt;code&gt;docName&lt;/code&gt;, the output filename; &lt;code&gt;docContent&lt;/code&gt;, the Base64-encoded source PDF; &lt;code&gt;htmlContent&lt;/code&gt;, a plain HTML string with inline CSS (not Base64-encoded, unlike the document itself); and &lt;code&gt;location&lt;/code&gt;, set to &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;Footer&lt;/code&gt;, or &lt;code&gt;Both&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;htmlContent&lt;/code&gt; field is the whole point. It is not a template reference or a design-tool export. It is a string like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"text-align: center; font-family: Arial; font-size: 12px; color: #666;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Confidential, Internal Use Only&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bold text, colored text, a logo pulled in as an embedded image, multi-column layout inside the header block, all of it is standard CSS applied to standard HTML, the same skills a frontend developer already has, applied to a part of a PDF that used to require learning a completely different, PDF-specific drawing model.&lt;/p&gt;

&lt;p&gt;Positioning does not require coordinate math either. &lt;code&gt;marginLeft&lt;/code&gt;, &lt;code&gt;marginRight&lt;/code&gt;, &lt;code&gt;marginTop&lt;/code&gt;, and &lt;code&gt;marginBottom&lt;/code&gt; are optional pixel values that reserve the space the header or footer content renders into, and &lt;code&gt;skipFirstPage&lt;/code&gt; excludes the very first page, useful for cover sheets that should not carry a repeated banner. &lt;code&gt;pages&lt;/code&gt; controls which pages get the treatment at all: an empty string for every page, &lt;code&gt;"1"&lt;/code&gt; for a single page, &lt;code&gt;"1,3,5"&lt;/code&gt; for specific pages, &lt;code&gt;"2-5"&lt;/code&gt; for a range, or &lt;code&gt;"1,3,7-10"&lt;/code&gt; for a mix of both. Everything runs synchronously by default and returns the finished PDF as Base64 &lt;code&gt;docContent&lt;/code&gt; in the response body; setting &lt;code&gt;async&lt;/code&gt; to true switches to a 202 response with a polling &lt;code&gt;Location&lt;/code&gt; header instead, which matters more once this is numbering hundreds of documents overnight than one file a user is waiting on.&lt;/p&gt;

&lt;h2&gt;
  
  
  In code
&lt;/h2&gt;

&lt;p&gt;The request is one HTTP call. Reading the source PDF, Base64-encoding it, and sending the styled HTML string is the entire client-side job:&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;time&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;# Base64-encoded key from the PDF4me 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/AddHtmlHeaderFooter&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;report.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;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;report.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;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;htmlContent&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;div style=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text-align:center;font-family:Arial;font-size:11px;color:#666;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Confidential, Internal Use Only&amp;lt;/div&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;location&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;Footer&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;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipFirstPage&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;marginBottom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;40.0&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;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="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;pdf_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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report-branded.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;pdf_bytes&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;span class="k"&gt;while&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;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;poll_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="n"&gt;pdf_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;poll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report-branded.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;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;skipFirstPage&lt;/code&gt; and &lt;code&gt;marginBottom&lt;/code&gt; above are the two fields doing the actual layout work: the margin reserves 40 pixels of footer space, and skipping the first page keeps a cover sheet clean. Swap &lt;code&gt;location&lt;/code&gt; to &lt;code&gt;"Header"&lt;/code&gt; or &lt;code&gt;"Both"&lt;/code&gt; and the same shape of request adds a header block, a footer block, or both in one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being a library problem
&lt;/h2&gt;

&lt;p&gt;A PDF manipulation library treats a header the same way it treats every other piece of content on the page: a shape or a text run placed at an absolute coordinate, styled through that library's own API surface, which is never quite the same surface twice across libraries or languages. Move to a different library, or a different language's port of the same library, and the styling code gets rewritten from scratch, because "bold, 10pt, right-aligned, in a specific shade of grey" means something different in every drawing API.&lt;/p&gt;

&lt;p&gt;CSS does not have that problem, because CSS is not a PDF concept. &lt;code&gt;font-weight: bold&lt;/code&gt;, &lt;code&gt;text-align: right&lt;/code&gt;, &lt;code&gt;color: #666666&lt;/code&gt;, these mean the same thing whether the HTML string is built in C#, Python, JavaScript, or Salesforce Apex, because none of those languages are the thing interpreting the styling. The rendering happens on PDF4me's side, once the request lands, so the calling code only ever needs to produce a string. That is a meaningfully smaller job than producing correct calls into a page-drawing API, and it is a job most backend teams already have the skills for without hiring for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing this endpoint is not
&lt;/h2&gt;

&lt;p&gt;PDF4me also has a dedicated &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-margin-to-pdf/" rel="noopener noreferrer"&gt;Add Margin to PDF&lt;/a&gt; endpoint, and it is worth being precise about what separates the two, because they sound related and solve different problems. Add Margin takes &lt;code&gt;marginLeft&lt;/code&gt;, &lt;code&gt;marginRight&lt;/code&gt;, &lt;code&gt;marginTop&lt;/code&gt;, and &lt;code&gt;marginBottom&lt;/code&gt; in millimeters, extends the page canvas by that amount, and pushes the existing content inward. What lands in that new space is nothing. It is blank. The endpoint's job is to make room, not to fill it.&lt;/p&gt;

&lt;p&gt;Add HTML Header Footer starts from the opposite assumption: there is already margin space at the top or bottom of the page, or &lt;code&gt;marginTop&lt;/code&gt;/&lt;code&gt;marginBottom&lt;/code&gt; on this endpoint reserve it, and the job is putting styled, meaningful content into that space. One endpoint creates emptiness. The other fills it. A workflow that needs both, more room at the top of the page and a styled banner inside that room, calls Add Margin first and Add Header Footer second, not one endpoint standing in for the other.&lt;/p&gt;

&lt;p&gt;There is a third related endpoint worth knowing about if the entire ask is a page number and nothing else: &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;Add Page Number to PDF&lt;/a&gt; handles that specific case directly, with its own alignment and format parameters, no HTML string required. If the number needs to sit inside a larger designed header alongside a logo or a document title, Add HTML Header Footer is still the right tool. If a number in the corner is the entire requirement, the dedicated endpoint is fewer parameters for the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the HTML before it hits a pipeline
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;htmlContent&lt;/code&gt; is a plain string built by application code, it is worth confirming the rendered output looks right before that string generation gets buried inside a workflow step nobody reopens for months. The &lt;a href="https://docs.pdf4me.com/url-api-tester/add-html-header-footer-to-pdf/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; runs this exact endpoint in the browser: upload a PDF, paste in the HTML and CSS, see the rendered result immediately, adjust the styling, try again. That loop, write CSS, see it rendered on an actual page, is a much shorter feedback cycle than deploying a change to a scheduled job and waiting for the next run to check whether the logo is centered.&lt;/p&gt;

&lt;p&gt;Full code samples covering this endpoint, starting with C#, are maintained in the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Edit/Add%20HTML%20Header%20Footer%20to%20PDF" rel="noopener noreferrer"&gt;pdf4me-api-samples repository&lt;/a&gt; alongside the documentation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same endpoint, four ways into a workflow
&lt;/h2&gt;

&lt;p&gt;The REST call is the mechanism. Most teams reach it through whichever automation platform already runs the rest of their document pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power Automate&lt;/strong&gt; exposes this as a connector action inside a Flow, so a styled header lands on a generated report the same step it gets produced, no separate branded-template maintenance. See &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/add-html-header-footer-pdf/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF in Power Automate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier&lt;/strong&gt; wires the same fields into a Zap step, useful for branding a document the moment it lands from a form submission or a CRM export. See &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/add-html-header-footer-to-pdf/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF in Zapier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make&lt;/strong&gt; does it as a module inside a scenario, typically placed right after a merge or convert step, so the finished, assembled document gets the header rather than each source file separately. See &lt;a href="https://docs.pdf4me.com/integration/make/edit/add-html-header-footer/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF in Make&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n&lt;/strong&gt; treats it as a node, which fits naturally into a self-hosted or code-adjacent pipeline where the HTML string itself might be built by an earlier code node before this one applies it. See &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-html-header-footer-to-pdf/" rel="noopener noreferrer"&gt;Add HTML Header Footer to PDF in n8n&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Across all four, the underlying contract matches the REST call exactly: the same &lt;code&gt;htmlContent&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, margins, and page-targeting options, exposed as connector fields instead of JSON keys. HTML and CSS validated once in the API Tester carries over unchanged into whichever platform actually runs the automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not replace
&lt;/h2&gt;

&lt;p&gt;This endpoint styles a header or footer block. It is not a full page-layout engine, and it is not a substitute for a PDF library on tasks that actually need one, precise multi-page table layout, form field placement, or complex vector graphics inside the document body itself are still library territory, or a different PDF4me endpoint's job entirely. The claim here is narrower and more honest than "never touch a PDF library again": for the specific, extremely common job of putting a styled banner, a logo, or a repeating line of text into the margin of every page, HTML and CSS already describe that layout better than a drawing API does, and reaching for the heavier tool for that one job is solving a smaller problem with a bigger hammer than it needs.&lt;/p&gt;

&lt;p&gt;The next time a ticket asks for a branded header across a batch of generated PDFs, that is markup and a POST request, not a coordinate system to relearn.&lt;/p&gt;

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

</description>
      <category>pdf</category>
      <category>api</category>
      <category>css</category>
      <category>automation</category>
    </item>
    <item>
      <title>Numbering Pages the Way Your Print Vendor Actually Wants Them</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 05 Aug 2026 11:11:24 +0000</pubDate>
      <link>https://dev.to/pdf4me/numbering-pages-the-way-your-print-vendor-actually-wants-them-5189</link>
      <guid>https://dev.to/pdf4me/numbering-pages-the-way-your-print-vendor-actually-wants-them-5189</guid>
      <description>&lt;p&gt;A print vendor's spec sheet does not say "add page numbers." It says something like: bottom center, format "# of {1}", 12pt, skip the cover page, 10mm margin from the bottom edge. Miss any one of those and the file bounces back with a rejection note, not a phone call explaining what to fix.&lt;/p&gt;

&lt;p&gt;That is the part page numbering tutorials skip. Adding a number to a PDF is not the hard part. Matching an exact, often oddly specific format that someone else defined, without touching the rest of the document's layout, is the actual job. Legal briefs want "- # -". Academic submissions want "Page # of {1}" top right. Some enterprise templates skip the first page entirely because it is a cover sheet. A page numbering tool that only prints "1, 2, 3" in the corner does not solve any of these, because none of them define what "properly numbered" means the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the API actually controls
&lt;/h2&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;Add Page Number to PDF&lt;/a&gt; endpoint is a single &lt;code&gt;POST /api/v2/AddPageNumber&lt;/code&gt; call. You send Base64-encoded PDF content and get Base64-encoded PDF content back, with the numbering already stamped in. Everything a print spec would ask for maps to a real parameter, not a workaround.&lt;/p&gt;

&lt;p&gt;The format itself is controlled by two placeholders inside a &lt;code&gt;pageNumberFormat&lt;/code&gt; string: &lt;code&gt;#&lt;/code&gt; for the current page number and &lt;code&gt;{1}&lt;/code&gt; for the total page count. That is enough to reproduce most of what shows up on a real spec sheet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"# of {1}"&lt;/code&gt; renders as 1 of 10, 2 of 10, 3 of 10&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"Page #"&lt;/code&gt; renders as Page 1, Page 2, Page 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"# / {1}"&lt;/code&gt; renders as 1 / 10, 2 / 10, 3 / 10&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"- # -"&lt;/code&gt; renders as - 1 -, - 2 -, - 3 -&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"[#/{1}]"&lt;/code&gt; renders as [1/10], [2/10], [3/10]&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"(#)"&lt;/code&gt; renders as (1), (2), (3)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Positioning is two required fields, &lt;code&gt;alignX&lt;/code&gt; (left, center, right) and &lt;code&gt;alignY&lt;/code&gt; (top, middle, bottom), so "bottom center" or "top right" is a direct instruction, not a guess involving pixel coordinates. If the spec gets more specific than that, &lt;code&gt;marginXinMM&lt;/code&gt; and &lt;code&gt;marginYinMM&lt;/code&gt; set the exact offset from the edge in millimeters, each accepting a 0-100 range.&lt;/p&gt;

&lt;p&gt;Styling covers the three things a vendor spec usually cares about: &lt;code&gt;fontSize&lt;/code&gt; (8-72), &lt;code&gt;isBold&lt;/code&gt;, and &lt;code&gt;isItalic&lt;/code&gt;. There is no font-family parameter, so if a spec calls for a specific typeface on the page numbers themselves, that is outside what this endpoint controls. Size, weight, and italics are what's available, and for the overwhelming majority of numbering requirements that is the whole ask anyway.&lt;/p&gt;

&lt;p&gt;The one option that solves a genuinely common annoyance on its own: &lt;code&gt;skipFirstPage&lt;/code&gt;. Set it to &lt;code&gt;true&lt;/code&gt; and page 1 (the cover, the title page) gets no number stamped on it at all, while the rest of the document numbers normally. No manual splitting the document into "cover" and "body" first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a vendor spec turns into, in practice
&lt;/h2&gt;

&lt;p&gt;Take a real example: a legal brief spec asking for "- # -" centered at the bottom of every page, 12pt, 10mm from the bottom edge, numbering starting on the first page (no cover to skip). That translates directly into the required and optional fields, nothing left to interpret:&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;"brief.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64 PDF&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pageNumberFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"alignX"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"center"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alignY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bottom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"marginYinMM"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fontSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skipFirstPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Swap &lt;code&gt;pageNumberFormat&lt;/code&gt; to &lt;code&gt;"Page # of {1}"&lt;/code&gt;, &lt;code&gt;alignX&lt;/code&gt; to &lt;code&gt;"right"&lt;/code&gt;, and &lt;code&gt;skipFirstPage&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, and the same call now matches an academic submission spec with a title page instead. Nothing about the request structure changes between the two, only the values, which is what makes this the kind of thing worth automating once rather than handling by hand every time a new document type shows up. Full runnable code samples, starting with C#, are maintained in the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Edit/Add%20Page%20Number%20to%20PDF" rel="noopener noreferrer"&gt;pdf4me-api-samples repository&lt;/a&gt; alongside the endpoint's own documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async for volume
&lt;/h2&gt;

&lt;p&gt;Everything above runs synchronously by default and returns a 200 with the numbered PDF immediately. For high-volume runs, set &lt;code&gt;async&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and the API returns a 202 with a &lt;code&gt;Location&lt;/code&gt; header instead. Poll that URL until it returns 200; the response body is the same shape either way: &lt;code&gt;docName&lt;/code&gt; and a Base64 &lt;code&gt;docContent&lt;/code&gt;. For a workflow numbering hundreds of report exports overnight rather than one document a user is waiting on, that distinction matters more than it looks like on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits next to header/footer stamping
&lt;/h2&gt;

&lt;p&gt;PDF4me also has a dedicated &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-header-footer-to-pdf/" rel="noopener noreferrer"&gt;Add HTML Header/Footer to PDF&lt;/a&gt; endpoint that supports page number variables inside a broader HTML-formatted header or footer, alongside logos, dates, or other repeating content. If page numbers are the only thing being added, the dedicated Add Page Number endpoint above is the more direct route: fewer parameters, no HTML to construct. If the number needs to live inside a designed header or footer block with other elements around it, that's the header/footer endpoint's job instead. Worth knowing both exist before building a header/footer template just to get a number in the corner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a format before it hits a pipeline
&lt;/h2&gt;

&lt;p&gt;Before wiring any of this into an automated workflow, it is worth confirming the exact format string against a sample PDF first. The &lt;a href="https://docs.pdf4me.com/url-api-tester/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; runs the Add Page Number endpoint directly in the browser: upload a file, set the parameters, see the numbered result, without writing a line of integration code yet. That is the fastest way to confirm "# of {1}" bottom center at 10mm margin is actually what a spec sheet means before that logic gets buried inside a Flow, a Zap, or a scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same endpoint, four ways into a workflow
&lt;/h2&gt;

&lt;p&gt;The REST endpoint is the mechanism; most teams reach it through whichever automation platform they already run everything else on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power Automate&lt;/strong&gt; exposes this as a connector action inside a Flow, so page numbering becomes one more step after a document generation or approval action, with no separate script to maintain. See &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/add-page-number-pdf/" rel="noopener noreferrer"&gt;Add Page Numbers to PDF in Power Automate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier&lt;/strong&gt; wires the same parameters into a Zap step, useful for numbering documents the moment they land from a form submission, a CRM export, or another connected app. See &lt;a href="https://docs.pdf4me.com/integration/zapier/edit/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;Add Page Numbers to PDF in Zapier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make&lt;/strong&gt; does it as a module inside a scenario, which is where this tends to sit well downstream of a merge or convert step, numbering the final assembled document rather than each source file separately. See &lt;a href="https://docs.pdf4me.com/integration/make/edit/add-page-number/" rel="noopener noreferrer"&gt;Add Page Numbers to PDF in Make&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n&lt;/strong&gt; treats it as a node, which fits a self-hosted or code-adjacent pipeline where page numbering is one node between a document-generation step and wherever the finished file gets delivered. See &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-page-number-to-pdf/" rel="noopener noreferrer"&gt;Add Page Number to PDF in n8n&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Across all four, the underlying contract is identical to the REST call: the same &lt;code&gt;pageNumberFormat&lt;/code&gt;, &lt;code&gt;alignX&lt;/code&gt;/&lt;code&gt;alignY&lt;/code&gt;, margins, and &lt;code&gt;skipFirstPage&lt;/code&gt; options, just exposed as connector fields instead of JSON keys. A format string validated once in the API Tester carries over exactly as-is into whichever platform actually runs the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;It stamps a number in a fixed format, position, and style, consistently across a document. It does not do mixed numbering schemes within a single file (roman numerals for a front-matter section, then 1, 2, 3 for the body, as some formal print specs require), and there is no font-family control beyond size, bold, and italic. There is also no way to restart the count partway through a document or offset the starting number away from 1, based on the documented parameter table; if a spec calls for either of those, that request sits outside this endpoint's scope.&lt;/p&gt;

&lt;p&gt;None of that makes it a smaller tool than it looks like at first. It makes it a more honest one. A single-purpose endpoint that takes six or seven parameters and reliably produces one specific, well-defined output is easier to build a pipeline around than a page-layout engine trying to do everything a design tool does. For the common case, a document that just needs consistent, correctly formatted page numbers wherever a spec says they should sit, that's exactly the scope of the job, and worth building for that scope rather than something heavier.&lt;/p&gt;

&lt;p&gt;The next time a print vendor's spec sheet lists an exact page number format, position, and margin, that is a parameter list, not a design problem.&lt;/p&gt;

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

</description>
      <category>pdf</category>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A PDF a Human Reads and a Machine Parses at the Same Time: How PDF4me Builds ZUGFeRD E-Invoices</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 03 Aug 2026 09:37:16 +0000</pubDate>
      <link>https://dev.to/pdf4me/a-pdf-a-human-reads-and-a-machine-parses-at-the-same-time-how-pdf4me-builds-zugferd-e-invoices-5enf</link>
      <guid>https://dev.to/pdf4me/a-pdf-a-human-reads-and-a-machine-parses-at-the-same-time-how-pdf4me-builds-zugferd-e-invoices-5enf</guid>
      <description>&lt;p&gt;Picture the scenario: your invoicing pipeline generates a clean, branded PDF for a German B2B customer. It looks right. It would print fine, email fine, and satisfy anyone who opens it by hand. Then it bounces, because since January 1, 2025, that customer is legally required to receive invoices in a format their software can parse without a human retyping the totals. A pretty PDF isn't enough anymore, and honestly, for a machine, it never really was the point.&lt;/p&gt;

&lt;p&gt;The part that surprises people who haven't dealt with this yet: the mandate doesn't force you to give up the human-readable PDF. It just requires that PDF to carry a second, structured version of itself, riding along inside it. That format is called ZUGFeRD, with an internationally aligned sibling called Factur-X.&lt;/p&gt;

&lt;p&gt;If you've never had to build one, it's worth understanding the mechanics before the code, because it's a genuinely clever piece of engineering, not just a compliance checkbox. So how does a single file manage to be both a human-readable invoice and a machine-parseable one at once?&lt;/p&gt;

&lt;h2&gt;
  
  
  What a ZUGFeRD invoice actually is
&lt;/h2&gt;

&lt;p&gt;Open a ZUGFeRD invoice in Adobe Acrobat or any PDF viewer and you see a normal invoice: logo, line items, totals, payment terms, nothing unusual. But embedded inside that same file, in its attachments, sits an XML document carrying the exact same invoice data in structured, typed form: invoice number, line items, tax rates, totals, every field an accounting system needs, tagged rather than buried in a paragraph a parser has to guess at.&lt;/p&gt;

&lt;p&gt;The container format making this possible is &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/create-pdfa/" rel="noopener noreferrer"&gt;PDF/A-3&lt;/a&gt;, the only PDF/A variant that permits attachments in any file format while still meeting the archival standard's long-term readability requirements. PDF/A-1 forbids embedded attachments outright, and PDF/A-2 allows embedded files only when those files are themselves PDF/A compliant; PDF/A-3 lifted that restriction to any format, which is exactly what lets an XML invoice ride along, and why every ZUGFeRD file you'll open is, underneath, a PDF/A-3b document with an XML file riding inside it.&lt;/p&gt;

&lt;p&gt;The embedded XML follows EN 16931, the EU's semantic data model for electronic invoices, with Germany's own XRechnung profile layered on top for domestic traffic. ZUGFeRD has shipped several versions (the 1.0 line through the current 2.x releases), each defining conformance levels, typically BASIC, COMFORT, EXTENDED, and the EN16931-aligned and XRECHNUNG profiles, trading structural strictness for how much invoice detail gets exposed to the machine-readable layer.&lt;/p&gt;

&lt;p&gt;This matters most directly for two kinds of teams: anyone generating outbound invoices for German business customers, and anyone receiving invoices who needs to parse the embedded XML straight into an ERP or accounting system instead of manually retyping totals off a PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two REST primitives underneath it
&lt;/h2&gt;

&lt;p&gt;Strip the standard's name away and ZUGFeRD generation is two REST calls chained together. First, &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/create-pdfa/" rel="noopener noreferrer"&gt;Create PDF/A&lt;/a&gt; converts the visible invoice into PDF/A-3b, one of eight conformance levels the endpoint supports (PDF/A-1b, PDF/A-1a, PDF/A-2b, PDF/A-2u, PDF/A-2a, PDF/A-3b, PDF/A-3u, PDF/A-3a). Second, &lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/add-attachment-to-pdf/" rel="noopener noreferrer"&gt;Add Attachment to PDF&lt;/a&gt; embeds the invoice XML inside that PDF/A-3 shell as a file attachment, the same mechanism you'd use to attach a spreadsheet to a report, just pointed at an XML payload instead. The endpoint's own schema makes this concrete: alongside the top-level &lt;code&gt;docContent&lt;/code&gt; and &lt;code&gt;docName&lt;/code&gt; for the base PDF, an &lt;code&gt;attachments&lt;/code&gt; array holds one object per file to embed, each carrying its own &lt;code&gt;docName&lt;/code&gt; (the attachment's filename as it appears inside the PDF, typically &lt;code&gt;invoice-data.xml&lt;/code&gt;) and &lt;code&gt;docContent&lt;/code&gt; (that file's own Base64 content).&lt;/p&gt;

&lt;p&gt;Here's what that looks like live-verified against &lt;code&gt;docs.pdf4me.com&lt;/code&gt;, chained end to end:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Base64-encoded, per PDF4me's Basic auth convention
&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Step 1: convert the invoice PDF to PDF/A-3b
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf_b64&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;pdfa_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_b64&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;invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compliance&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;PdfA3b&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;allowUpgrade&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;allowDowngrade&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="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;pdfa_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/PdfA&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;pdfa_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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: embed the invoice XML inside the PDF/A-3b shell
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice-data.xml&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;xml_b64&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;attach_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;pdfa_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;docContent&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;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attachments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;invoice-data.xml&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;xml_b64&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;zugferd_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/AddAttachmentToPdf&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;attach_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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# zugferd_resp["docContent"] is now the finished ZUGFeRD PDF/A-3b file, Base64-encoded
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest flag: PDF4me's own Create PDF/A docs page renders its response example inconsistently with every other endpoint here, so double check the exact response key your account actually returns before wiring &lt;code&gt;pdfa_resp["docContent"]&lt;/code&gt; into production. Every other call in this chain uses consistent &lt;code&gt;docContent&lt;/code&gt;/&lt;code&gt;docName&lt;/code&gt; naming, and it's a safe bet Create PDF/A does too, but verify against a real response rather than trusting a docs page's formatting on this one.&lt;/p&gt;

&lt;p&gt;Going the other direction, &lt;a href="https://docs.pdf4me.com/pdf4me-api/extract/extract-attachment-from-pdf/" rel="noopener noreferrer"&gt;Extract Attachment from PDF&lt;/a&gt; does the reverse: hand it a ZUGFeRD PDF, get back the embedded XML as structured data, live-verified as an &lt;code&gt;outputDocuments&lt;/code&gt; array of &lt;code&gt;{fileName, streamFile}&lt;/code&gt; objects, one per embedded file, exactly what a receiving ERP system needs to actually consume the invoice instead of just archiving it unread.&lt;/p&gt;

&lt;p&gt;Knowing this matters even if you never touch the REST API directly. It explains what PDF4me's no-code action is doing under the hood, and it tells you where to look if a generated file doesn't validate the way you expect.&lt;/p&gt;

&lt;p&gt;Which approach to reach for depends on how much control the job needs. Calling Create PDF/A and Add Attachment to PDF yourself makes sense if you're already generating the invoice PDF through your own template engine and just need the final embedding step, or if you want to validate the XML against your own schema before it goes anywhere near the PDF. Reaching for Create ZUGFeRD Invoice directly makes more sense the moment you're already living inside Make, Zapier, Power Automate, or n8n for the rest of the invoicing flow, since it collapses both REST calls, plus the conformance-level bookkeeping, into a single configured step you're not maintaining yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-action version
&lt;/h2&gt;

&lt;p&gt;Chaining two REST calls yourself is a reasonable way to build this, but PDF4me also ships it as a single purpose-built action, Create ZUGFeRD Invoice, across all four automation platforms this cluster covers. Feed it invoice data as XML, JSON, or CSV, pick a conformance level, and get back a hybrid PDF/A-3 file with the XML embedded and aligned to EN 16931. No manual REST chaining required.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://docs.pdf4me.com/integration/make/pdf/create-zugferd-invoice/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, the action accepts invoice data in any of those three formats and returns the ready-to-send hybrid file. A documented &lt;a href="https://docs.pdf4me.com/blog/create-zugferd-invoice-make-dropbox-workflow/" rel="noopener noreferrer"&gt;four-module Dropbox workflow&lt;/a&gt; walks through the whole thing: pull a base PDF and a ZUGFeRD 2.0+ XML file from Dropbox, run them through Create ZUGFeRD Invoice at EN16931 conformance, push the finished hybrid invoice back to Dropbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/pdf/create-zugferd-invoice/" rel="noopener noreferrer"&gt;Zapier's&lt;/a&gt; version supports ZUGFeRD 1.0 and the full 2.x line, across BASIC, COMFORT, EXTENDED, EN16931, and XRECHNUNG conformance. Its &lt;a href="https://docs.pdf4me.com/blog/zugferd-invoice-zapier-dropbox-xml-workflow/" rel="noopener noreferrer"&gt;Dropbox + XML walkthrough&lt;/a&gt; triggers on a new file landing in Dropbox, fetches the base PDF and XML payload, runs Create ZUGFeRD Invoice with XmlWithPdf output, and saves the result back to Dropbox at BASIC conformance in the documented example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf/create-zugferd-invoice/" rel="noopener noreferrer"&gt;Power Automate's&lt;/a&gt; action is built explicitly around the German B2B mandate, and pulls source data from SharePoint, OneDrive, Outlook, or Dataverse in addition to XML, JSON, or CSV. Two documented walkthroughs cover the two source-format paths you're most likely to hit: a &lt;a href="https://docs.pdf4me.com/blog/zugferd-invoice-power-automate-dropbox-json-workflow/" rel="noopener noreferrer"&gt;Dropbox + JSON flow&lt;/a&gt; for teams generating invoice data programmatically, and a &lt;a href="https://docs.pdf4me.com/blog/zugferd-invoice-power-automate-dropbox-xml-workflow/" rel="noopener noreferrer"&gt;Dropbox + XML flow&lt;/a&gt; for teams that already have ZUGFeRD-formatted XML sitting in a system somewhere and just need it embedded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/pdf/create-zugferd-invoice-latest/" rel="noopener noreferrer"&gt;n8n's&lt;/a&gt; action generates ZUGFeRD 2.0 through 2.4, with EN16931, XRECHNUNG, BASIC, and EXTENDED conformance, from XML, JSON, or CSV. Its &lt;a href="https://docs.pdf4me.com/blog/zugferd-invoice-n8n-dropbox-json-workflow/" rel="noopener noreferrer"&gt;Dropbox + JSON walkthrough&lt;/a&gt; uploads invoice JSON, runs it through the ZUGFeRD action, and lands the finished file back in Dropbox, with real screenshots of every mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't do for you
&lt;/h2&gt;

&lt;p&gt;A structurally valid ZUGFeRD file isn't automatically an invoice your counterparty's accounts-payable system will accept without complaint. Conformance levels exist precisely because different trading partners and national profiles expect different amounts of structured detail, and whether your invoice data actually satisfies EN 16931's business rules is a data-quality question the generation step alone doesn't answer for you. PDF4me's &lt;a href="https://docs.pdf4me.com/integration/make/pdf/validate-pdfa/" rel="noopener noreferrer"&gt;Validate PDF/A&lt;/a&gt; action can confirm the PDF/A-3 container itself conforms to ISO 19005, a useful gate before anything ships, though it checks the container, not ZUGFeRD's own business-rule validation of the embedded XML. That business-rule check isn't a single fixed target either: EN 16931's rules ship as a Schematron ruleset, and the profiles layered on top, XRechnung and each country's CIUS, pin their own ruleset releases on their own schedules. A file that validates cleanly today can fail after one of those releases updates, with nothing in your own code having changed. Store the ruleset version that validated each invoice alongside the pass/fail result, not just the boolean, so you can still answer "was this invoice valid when we sent it" months later. Build that check into your pipeline before this touches production traffic, not after the first rejected invoice comes back.&lt;/p&gt;

&lt;p&gt;Which conformance level to pick isn't really a technical question, it's a contractual one. Ask the receiving business, or their invoicing software vendor, which profile they expect. EN16931 and XRECHNUNG are the two most commonly required for the German mandate specifically, so match what's asked for rather than defaulting to EXTENDED because it sounds more thorough.&lt;/p&gt;

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

&lt;p&gt;Every action above sits behind the same &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;PDF4me V2 REST API&lt;/a&gt;, so the authentication and base URL story is identical whether you're calling Create PDF/A and Add Attachment to PDF yourself, or letting Create ZUGFeRD Invoice do both in one step through whichever automation platform your team already runs on.&lt;/p&gt;

&lt;p&gt;Germany's mandate is the concrete deadline in front of anyone shipping invoices there today. Building the pipeline once, with a tool that already understands PDF/A-3's attachment mechanics, beats re-explaining to a new hire every few months why an invoice PDF has a file attached to it.&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>einvoicing</category>
      <category>api</category>
      <category>compliance</category>
      <category>automation</category>
    </item>
    <item>
      <title>Text Stamps vs. Watermarks: Same Visual Result, Two Different API Calls</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 03 Aug 2026 03:39:39 +0000</pubDate>
      <link>https://dev.to/pdf4me/text-stamps-vs-watermarks-same-visual-result-two-different-api-calls-icm</link>
      <guid>https://dev.to/pdf4me/text-stamps-vs-watermarks-same-visual-result-two-different-api-calls-icm</guid>
      <description>&lt;p&gt;Ask a developer to add a "watermark" to a PDF and they picture gray text on a diagonal, something like CONFIDENTIAL bleeding through the page. Ask for a "stamp" and they picture a logo in the corner. PDF4me's own docs use both words for both things, sometimes on the same page. That's not sloppy naming. It reflects a real split under the hood: two separate REST endpoints for what looks, on screen, like one feature.&lt;/p&gt;

&lt;p&gt;Send an image file to the endpoint built for text, or a hex color code to the one built for images, and you'll get a 500 that has nothing to do with your API key. Here's exactly where the line sits between the two, verified against the live docs and the official sample repo, plus what happens to the naming once you leave raw REST for Power Automate, Make, Zapier, or n8n.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication, once, for both endpoints
&lt;/h2&gt;

&lt;p&gt;Every PDF4me REST call needs a Base64-encoded API key in the &lt;code&gt;Authorization&lt;/code&gt; header. Full details, including how to get a key and encode it, live in &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to the PDF4me V2 API&lt;/a&gt;. Get that working once and both endpoints below are ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Text Stamp to PDF: &lt;code&gt;POST /api/v2/Stamp&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/text-stamp/" rel="noopener noreferrer"&gt;Add Text Stamp to PDF&lt;/a&gt; renders a string onto a page and gives you granular control over how it looks. Required fields: &lt;code&gt;docName&lt;/code&gt;, &lt;code&gt;docContent&lt;/code&gt; (Base64 source PDF), &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;alignX&lt;/code&gt;, &lt;code&gt;alignY&lt;/code&gt;. The optional fields are where the control lives: &lt;code&gt;fontName&lt;/code&gt; (Arial, Times New Roman, Helvetica, Courier New), &lt;code&gt;fontSize&lt;/code&gt; (8-72), &lt;code&gt;fontColor&lt;/code&gt; as hex, &lt;code&gt;isBold&lt;/code&gt;/&lt;code&gt;isItalics&lt;/code&gt;/&lt;code&gt;underline&lt;/code&gt;, &lt;code&gt;rotate&lt;/code&gt; (0, 45, 90, or -45), &lt;code&gt;opacity&lt;/code&gt; (0-100), &lt;code&gt;isBackground&lt;/code&gt; to push it behind existing content, &lt;code&gt;showOnlyInPrint&lt;/code&gt;, and &lt;code&gt;fitTextOverPage&lt;/code&gt; to auto-scale the text to the page.&lt;/p&gt;

&lt;p&gt;Verified live against &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Edit/Add%20Text%20Stamp%20to%20PDF/Python/Add%20Text%20Stamp%20To%20PDF" rel="noopener noreferrer"&gt;the official Python sample&lt;/a&gt; (MIT licensed), the core request 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;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;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/Stamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;pages&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;all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONFIDENTIAL - PDF4me Watermark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alignX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# lowercase: left, center, right
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alignY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;middle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# lowercase: top, middle, bottom
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&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;fontName&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;Arial&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;fontSize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fontColor&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;#FF0000&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;isBold&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;rotate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isBackground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; returns &lt;code&gt;docName&lt;/code&gt; and &lt;code&gt;docContent&lt;/code&gt; (the stamped PDF, Base64) directly. A &lt;code&gt;202&lt;/code&gt; means async processing kicked in: pull the &lt;code&gt;Location&lt;/code&gt; header and poll it until you get a &lt;code&gt;200&lt;/code&gt; with the same JSON shape. That polling loop is fully worked out in the sample repo linked above, worth reading before you write your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Image Stamp to PDF: &lt;code&gt;POST /api/v2/ImageStamp&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/edit/image-stamp/" rel="noopener noreferrer"&gt;Add Image Stamp to PDF&lt;/a&gt; is a different endpoint entirely, and the required fields make that obvious immediately: &lt;code&gt;docName&lt;/code&gt;, &lt;code&gt;docContent&lt;/code&gt;, &lt;code&gt;imageName&lt;/code&gt;, &lt;code&gt;imageFile&lt;/code&gt; (your logo, also Base64), &lt;code&gt;alignX&lt;/code&gt;, &lt;code&gt;alignY&lt;/code&gt;. No &lt;code&gt;text&lt;/code&gt; field. No &lt;code&gt;fontColor&lt;/code&gt;. In their place: &lt;code&gt;heightInMM&lt;/code&gt;/&lt;code&gt;widthInMM&lt;/code&gt; (10-200mm) or &lt;code&gt;heightInPx&lt;/code&gt;/&lt;code&gt;widthInPx&lt;/code&gt; (20-600px) for sizing, and support for JPG, PNG, GIF, and other standard formats.&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;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/ImageStamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="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;logo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;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;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imageFile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;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;imageName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alignX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# capitalized here: Left, Center, Right
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alignY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Middle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# capitalized here: Top, Middle, Bottom
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heightInMM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;widthInMM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isBackground&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;alignX&lt;/code&gt;/&lt;code&gt;alignY&lt;/code&gt; values flip capitalization between the two endpoints, lowercase on &lt;code&gt;Stamp&lt;/code&gt;, capitalized on &lt;code&gt;ImageStamp&lt;/code&gt;. That inconsistency is real and confirmed directly against both live docs pages, not a typo in this article. It's the kind of thing that passes code review and fails at runtime.&lt;/p&gt;

&lt;p&gt;The other tell: there's no &lt;code&gt;rotate&lt;/code&gt; parameter anywhere on the image endpoint. Need a rotated logo? Rotate the source image before you send it. This API call won't do it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right one
&lt;/h2&gt;

&lt;p&gt;The rule: a string of characters goes through &lt;code&gt;/api/v2/Stamp&lt;/code&gt;. A graphic, logo, scanned signature, seal, goes through &lt;code&gt;/api/v2/ImageStamp&lt;/code&gt;. Plenty of real documents need both in sequence, a signed contract might carry an image stamp of a signature in one corner and a text stamp reading EXECUTED diagonally across the page. Chain the calls: the &lt;code&gt;docContent&lt;/code&gt; one endpoint returns is valid input to the next.&lt;/p&gt;

&lt;p&gt;Neither endpoint is an "upgrade" of the other. &lt;code&gt;fitTextOverPage&lt;/code&gt; will never produce a logo no matter how you tune it, and &lt;code&gt;ImageStamp&lt;/code&gt; has no &lt;code&gt;text&lt;/code&gt; field to put a dynamic invoice number in. If your watermark needs to vary per document, that's the text endpoint. If it needs to stay pixel-identical across every file, that's the image endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naming shifts again outside raw REST
&lt;/h2&gt;

&lt;p&gt;Cross into PDF4me's no-code integrations and the labels don't hold. &lt;a href="https://docs.pdf4me.com/integration/power-automate/edit/add-text-stamp-pdf/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-text-stamp-to-pdf/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; keep "Stamp" as separate &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-text-stamp-to-pdf/" rel="noopener noreferrer"&gt;Text Stamp&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/edit/add-image-stamp-to-pdf/" rel="noopener noreferrer"&gt;Image Stamp&lt;/a&gt; actions. &lt;a href="https://docs.pdf4me.com/integration/make/edit/add-text-watermark/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; drops "Stamp" entirely for &lt;a href="https://docs.pdf4me.com/integration/make/edit/add-text-watermark/" rel="noopener noreferrer"&gt;Text Watermark&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/make/edit/add-image-watermark/" rel="noopener noreferrer"&gt;Image Watermark&lt;/a&gt;. &lt;a href="https://docs.pdf4me.com/integration/zapier/image/add-text-watermark-to-pdf/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; follows Make's convention with &lt;a href="https://docs.pdf4me.com/integration/zapier/image/add-text-watermark-to-pdf/" rel="noopener noreferrer"&gt;Add Text Watermark to PDF&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/zapier/image/add-image-watermark-to-pdf/" rel="noopener noreferrer"&gt;Add Image Watermark to PDF&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Same &lt;code&gt;Stamp&lt;/code&gt;/&lt;code&gt;ImageStamp&lt;/code&gt; behavior under all four, different search term depending on where you're building. Search "stamp" in Make and you'll come up empty. Search "watermark" in Power Automate and same problem, reversed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm the shape before it hits production
&lt;/h2&gt;

&lt;p&gt;Rather than debug a live workflow over a flipped capitalization or a missing field, run either endpoint through the API Tester first: &lt;a href="https://docs.pdf4me.com/url-api-tester/add-text-stamp-to-pdf/" rel="noopener noreferrer"&gt;text stamp tester&lt;/a&gt; or &lt;a href="https://docs.pdf4me.com/url-api-tester/add-image-stamp-to-pdf/" rel="noopener noreferrer"&gt;image stamp tester&lt;/a&gt;. See the exact request and response before it touches a document that matters.&lt;/p&gt;

&lt;p&gt;Two endpoints, two request shapes, at least four labels for the same idea depending on which product surface you're standing in. Know which word your platform uses, know which field set your content needs, and the rest is filling in a form.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>pdf</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Watermarking Images and Auto-Correcting Sideways Photos with One API Call</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:42:41 +0000</pubDate>
      <link>https://dev.to/pdf4me/watermarking-images-and-auto-correcting-sideways-photos-with-one-api-call-5gdb</link>
      <guid>https://dev.to/pdf4me/watermarking-images-and-auto-correcting-sideways-photos-with-one-api-call-5gdb</guid>
      <description>&lt;p&gt;Somebody on your team is opening an image editor to stamp a logo onto a batch of product photos. Somebody else, a few tabs over, is manually rotating a stack of scanned receipts that came in sideways from a phone camera. Neither person thinks of the other's task as related to their own, but structurally they're the same job: send an image in, get a modified image back, repeat it a thousand times without touching a mouse.&lt;/p&gt;

&lt;p&gt;Below are three PDF4me &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/add-image-watermark-to-image/" rel="noopener noreferrer"&gt;image API&lt;/a&gt; endpoints that cover both problems, with working Python samples for each, verified against the live parameter tables and the base URL from the &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;Connect to PDF4me API guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stamping a logo onto a photo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/image/add-image-watermark-to-image/" rel="noopener noreferrer"&gt;Add Image Watermark&lt;/a&gt; overlays one image, typically a logo or brand mark, onto another. This is compositing, not PDF-page watermarking: control over position, opacity, and rotation is built into the request.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;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;/api/v2/AddImageWatermarkToImage&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;product-photo.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;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="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;logo.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;watermark_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product-photo.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;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;WatermarkFileName&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;logo.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;WatermarkFileContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;watermark_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;Position&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;bottomright&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PositionX&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PositionY&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rotation&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="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;base_url&lt;/span&gt; &lt;span class="o"&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;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watermarked-photo.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;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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Position&lt;/code&gt; accepts &lt;code&gt;topright&lt;/code&gt;, &lt;code&gt;topleft&lt;/code&gt;, &lt;code&gt;bottomright&lt;/code&gt;, &lt;code&gt;bottomleft&lt;/code&gt;, &lt;code&gt;centralhorizontal&lt;/code&gt;, &lt;code&gt;diagonal&lt;/code&gt;, &lt;code&gt;centralvertical&lt;/code&gt;, or &lt;code&gt;custom&lt;/code&gt; (paired with &lt;code&gt;PositionX&lt;/code&gt;/&lt;code&gt;PositionY&lt;/code&gt;). The watermark asset itself is usually fixed, one logo file reused across a whole batch, so this is a parameterized call, not a design decision.&lt;/p&gt;

&lt;p&gt;Same operation, no-code: Make's &lt;a href="https://docs.pdf4me.com/integration/make/image/add-image-watermark/" rel="noopener noreferrer"&gt;Add Image Watermark to Image&lt;/a&gt;, Zapier's &lt;a href="https://docs.pdf4me.com/integration/zapier/image/add-image-watermark-to-image/" rel="noopener noreferrer"&gt;Add Image Watermark To Image&lt;/a&gt;, n8n's &lt;a href="https://docs.pdf4me.com/integration/n8n/image/add-image-watermark-to-image/" rel="noopener noreferrer"&gt;Add Image Watermark To Image node&lt;/a&gt;. The &lt;a href="https://docs.pdf4me.com/url-api-tester/add-image-watermark-to-image/" rel="noopener noreferrer"&gt;interactive API Tester&lt;/a&gt; runs the call against a real image in the browser if you want to see placement before wiring anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stamping text instead of a logo
&lt;/h2&gt;

&lt;p&gt;Not every watermark is a static image. A case number, a capture date, a "confidential" label: these are generated per file, which is exactly what a static overlay image can't do. &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/add-text-watermark-to-image/" rel="noopener noreferrer"&gt;Add Text Watermark&lt;/a&gt; controls font, size, color, and opacity through the request itself.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;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;/api/v2/AddTextWatermarkToImage&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;delivery-photo.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;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;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;delivery-photo.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;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;WatermarkText&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;CASE-4471&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;TextPosition&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;bottomleft&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;TextFontFamily&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;Arial&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;TextFontSize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TextColour&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;#ffffff&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;IsBold&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;IsUnderline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IsItalic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RotationAngle&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PositionX&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PositionY&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="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;base_url&lt;/span&gt; &lt;span class="o"&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;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WatermarkText&lt;/code&gt; is the one field that makes this dynamic: pull it from a spreadsheet row, a timestamp, or a record in whatever system triggered the call.&lt;/p&gt;

&lt;p&gt;No-code equivalents: Make's &lt;a href="https://docs.pdf4me.com/integration/make/image/add-text-watermark/" rel="noopener noreferrer"&gt;Add Text Watermark to Image&lt;/a&gt;, Zapier's &lt;a href="https://docs.pdf4me.com/integration/zapier/image/add-text-watermark-to-image/" rel="noopener noreferrer"&gt;Add Text Watermark To Image&lt;/a&gt;, n8n's &lt;a href="https://docs.pdf4me.com/integration/n8n/image/add-text-watermark-to-image/" rel="noopener noreferrer"&gt;Add Text Watermark To Image&lt;/a&gt;. The &lt;a href="https://docs.pdf4me.com/url-api-tester/add-text-watermark-to-image/" rel="noopener noreferrer"&gt;API Tester's text-watermark page&lt;/a&gt; is the fastest way to see what a given font and opacity setting actually produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why phones hand you sideways photos in the first place
&lt;/h2&gt;

&lt;p&gt;Camera sensors don't physically rotate when you turn the phone. Instead, the device writes an EXIF orientation tag, a number that tells a viewer how to rotate the image before displaying it. Software that reads that tag shows the photo upright. Software that doesn't shows exactly what the sensor captured, and that file stays wrong the moment it leaves an EXIF-aware viewer for a report, an email, or a print vendor's system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/image/rotate-image-by-exif-data/" rel="noopener noreferrer"&gt;Rotate Image by EXIF Data&lt;/a&gt; reads that tag and actually rotates the pixels to match it, so the file is correct everywhere afterward. It only takes two fields.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;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;/api/v2/RotateImageByExifData&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;driver-photo.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;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;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;driver-photo.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;doc_content&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="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;base_url&lt;/span&gt; &lt;span class="o"&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;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;driver-photo-upright.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;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a genuinely different job from PDF4me's plain &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/rotate-image/" rel="noopener noreferrer"&gt;Rotate Image&lt;/a&gt; endpoint, which turns an image by a fixed angle you specify yourself. Fixed-angle rotation works when you already know the exact correction a whole batch needs. EXIF-based rotation is for when you don't, and can't, because every incoming photo might carry a different tag depending on how it was captured.&lt;/p&gt;

&lt;p&gt;No-code equivalents: Make's &lt;a href="https://docs.pdf4me.com/integration/make/image/rotate-by-exif-data/" rel="noopener noreferrer"&gt;Auto-Rotate Images by EXIF Data&lt;/a&gt;, n8n's &lt;a href="https://docs.pdf4me.com/integration/n8n/image/rotate-image-by-exif-data/" rel="noopener noreferrer"&gt;Rotate Image By EXIF Data node&lt;/a&gt;. The &lt;a href="https://docs.pdf4me.com/url-api-tester/rotate-image-by-exif-data/" rel="noopener noreferrer"&gt;API Tester&lt;/a&gt; confirms what a specific file's tag will actually do before it's wired into anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the no-code coverage runs out
&lt;/h2&gt;

&lt;p&gt;Worth saying plainly: Power Automate has no published action for image watermarking or EXIF-based rotation as of this writing, so a Power Automate-only pipeline needs to call the REST API directly for these three operations. Zapier's rotate action (&lt;a href="https://docs.pdf4me.com/integration/zapier/image/rotate-image/" rel="noopener noreferrer"&gt;Rotate Image&lt;/a&gt;) only covers the fixed-angle version, not the EXIF-aware one; n8n offers &lt;a href="https://docs.pdf4me.com/integration/n8n/image/rotate-image/" rel="noopener noreferrer"&gt;both separately&lt;/a&gt; too. If auto-rotation by orientation tag is the actual requirement and Zapier is the only automation layer available, that step currently needs to live somewhere else in the pipeline. Neither gap is a reason to avoid the API, it's a reason to check platform by platform before assuming a specific no-code action already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next once the file is correct
&lt;/h2&gt;

&lt;p&gt;Two related endpoints sit one step away from everything above. Once an orientation tag has done its job, some pipelines strip EXIF data out entirely before a file goes anywhere external, since it can carry GPS coordinates and camera details nobody meant to publish: &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/remove-exif-tags-from-image/" rel="noopener noreferrer"&gt;Remove EXIF Tags from Image&lt;/a&gt; does that. And if you need to see what's actually in a file's metadata before deciding what to do with it, &lt;a href="https://docs.pdf4me.com/pdf4me-api/image/get-image-metadata/" rel="noopener noreferrer"&gt;Get Image Metadata&lt;/a&gt; reads it back as structured data instead of guesswork.&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>image</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pulling plain text out of a Word document with one call, no Word install required</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:56:34 +0000</pubDate>
      <link>https://dev.to/pdf4me/pulling-plain-text-out-of-a-word-document-with-one-call-no-word-install-required-2nb6</link>
      <guid>https://dev.to/pdf4me/pulling-plain-text-out-of-a-word-document-with-one-call-no-word-install-required-2nb6</guid>
      <description>&lt;p&gt;"Extract the text from this Word document" sounds like a one-line task until the document turns out to have three rounds of tracked changes, a comment thread in the margins, and a footer repeated on every page. A naive extraction pass hands back all of it mixed together, with no way to tell the kept text from the deleted text once both have become "content." PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/extract/extract-text-from-word/" rel="noopener noreferrer"&gt;Extract Text from Word API&lt;/a&gt; is built specifically to give you control over that, not just pull raw text out of a .docx file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why plain OOXML parsing is not enough
&lt;/h2&gt;

&lt;p&gt;Two paths usually get tried first. One is installing Microsoft Word or Office on a server and driving it through an interop library, which works until licensing at scale and unstable headless Office instances turn a document-processing job into an ops problem. The other is unzipping the .docx and parsing the OOXML XML directly, which works until a document with tracked changes or embedded comments shows up and the output is full of reviewer names, deletion markup, and boilerplate that never belonged in the final text.&lt;/p&gt;

&lt;p&gt;The REST endpoint sidesteps both: send a Base64-encoded .docx, get back clean text, with four content-filtering options doing the real work.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.pdf4me.com/api/v2/ExtractTextFromWord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Required parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docName&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Source file name, with or without the .docx/.doc extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docContent&lt;/td&gt;
&lt;td&gt;Base64 (String)&lt;/td&gt;
&lt;td&gt;The Word document content, Base64-encoded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StartPageNumber&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;First page to extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EndPageNumber&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;Last page to extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RemoveComments&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;Strip comment text from the output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RemoveHeaderFooter&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;Strip repeated header/footer content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AcceptChanges&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;Accept tracked changes before extracting, rather than rejecting them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Optional:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;async&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;When true, returns 202 Accepted with a Location header to poll instead of blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A synchronous call returns 200 OK with:&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;"extractedText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Extracted text content from Word document pages 1 to 3..."&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;"output.txt"&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;h2&gt;
  
  
  A working Python example
&lt;/h2&gt;

&lt;p&gt;Verified against the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples/tree/main/Extract/Extract%20Text%20from%20word/Python/Extract%20Text%20from%20word" rel="noopener noreferrer"&gt;official Python sample&lt;/a&gt; in pdf4me/pdf4me-api-samples (MIT licensed), field names and endpoint match the live docs page exactly:&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 https://dev.pdf4me.com/dashboard/#/api-keys/
&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/ExtractTextFromWord&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.docx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.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;StartPageNumber&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndPageNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RemoveComments&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;RemoveHeaderFooter&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;AcceptChanges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;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="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="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;extractedText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large documents, set "async": True instead. The API returns 202 Accepted with a Location header, and polling that URL with the same Authorization header until it returns 200 gets the same extractedText payload once processing finishes. The official sample script handles both paths, sync and the polling loop, in one file, worth a look before writing your own retry logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same options, in a no-code flow
&lt;/h2&gt;

&lt;p&gt;The four content-filtering options are not REST-only. The &lt;a href="https://docs.pdf4me.com/integration/n8n/extract/extract-text-from-word/" rel="noopener noreferrer"&gt;n8n node&lt;/a&gt; exposes the same page range, comment, header/footer, and tracked-changes controls, and accepts input as binary data, Base64, or a URL, whichever fits the flow already moving the file around. The &lt;a href="https://docs.pdf4me.com/integration/zapier/word/extract-text-from-word/" rel="noopener noreferrer"&gt;Zapier action&lt;/a&gt;, the &lt;a href="https://docs.pdf4me.com/integration/make/word/extract-text/" rel="noopener noreferrer"&gt;Make module&lt;/a&gt;, and the &lt;a href="https://docs.pdf4me.com/integration/power-automate/extract/extract-text-word/" rel="noopener noreferrer"&gt;Power Automate action&lt;/a&gt; all describe the identical underlying behavior, just as configuration fields inside their own flow builders. A team automating this inside Power Automate as part of a Microsoft 365 approval flow is calling the same extraction logic a backend service hits directly over REST. There is one engine under all five surfaces, not five different implementations to keep in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it before wiring it up
&lt;/h2&gt;

&lt;p&gt;Comment and tracked-change handling is configurable, not automatic, and every document set carries its own editing history. Before committing to a set of flags in code, PDF4me's &lt;a href="https://docs.pdf4me.com/url-api-tester/extract-text-from-word/" rel="noopener noreferrer"&gt;interactive API Tester&lt;/a&gt; lets you upload a real .docx, toggle the page range and filtering options, and see the extracted text come back in the browser first. It catches a mismatched assumption about a document set before that assumption is baked into a pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a key and the request contract
&lt;/h2&gt;

&lt;p&gt;New to the API: the &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Getting Started guide&lt;/a&gt; covers account creation, generating a key from the developer dashboard, and making a first authenticated call. Every endpoint, this one included, follows the same &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;V2 REST API contract&lt;/a&gt;: POST with a JSON body, the file as Base64 in docContent or a public URL in docUrl, docName carrying the extension that tells PDF4me which engine to route to, and a Base64-encoded API key sent as a Basic auth header.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it will not do
&lt;/h2&gt;

&lt;p&gt;This endpoint returns plain text, not structure. Tables come back as text, not as tables, and heading-versus-body distinctions are not preserved, so anything downstream that needs document structure intact needs a different step, not this one. And because comment and tracked-change handling is a choice rather than a default, it is worth deciding upfront, per document type, whether "final text only" or "everything including edit history" is the actual goal, since those are two different configurations of the same call, not two outputs from one default request.&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>word</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Branding and Cleaning Up Spreadsheets Without Opening Excel</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:15:30 +0000</pubDate>
      <link>https://dev.to/pdf4me/branding-and-cleaning-up-spreadsheets-without-opening-excel-cka</link>
      <guid>https://dev.to/pdf4me/branding-and-cleaning-up-spreadsheets-without-opening-excel-cka</guid>
      <description>&lt;p&gt;Someone on your finance team exports a report every Friday. Before it goes out: a "DRAFT" watermark goes across every sheet, the company logo lands in the header, and the internal notes tab gets deleted before anyone outside the building sees it. Right now a human does this by hand in Excel, every week, and every so often forgets the third step.&lt;/p&gt;

&lt;p&gt;None of that requires Excel to be open. It requires a few API calls.&lt;/p&gt;

&lt;p&gt;PDF4me treats an Excel workbook as something a server can edit directly: watermark it, stamp a header or footer onto it, add or remove a worksheet, all without Excel installed anywhere in the pipeline. That matters more than it sounds like it should. Most teams don't have a spare Excel license sitting on a server, and the alternative (a VBA macro that only runs on someone's desktop, by hand, when they remember) is exactly the manual step automation is supposed to remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watermarking a workbook the same way you'd watermark a PDF
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/add-text-watermark/" rel="noopener noreferrer"&gt;Add Text Watermark&lt;/a&gt; does for Excel what a text stamp does for a PDF: font, transparency, and positioning are parameters you set once and reuse. "CONFIDENTIAL" across an HR export, "DRAFT" across a report still in review, an internal classification label before a workbook lands in the wrong inbox: it's the same call with a different string.&lt;/p&gt;

&lt;p&gt;The endpoint is a straight POST, confirmed against the live docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.pdf4me.com/api/v2/office/ApiV2Excel/ExcelAddWatermark
Content-Type: application/json
Authorization: Basic YOUR_BASE64_ENCODED_API_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;data.xlsx&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;document&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;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;data.xlsx&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;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;addWatermarkToExcelAction&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;watermarkText&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&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;fontFamily&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;Arial&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;fontSize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fontColor&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;#808080&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;semiTransparent&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="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/office/ApiV2Excel/ExcelAddWatermark&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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;watermarkText&lt;/code&gt; is the only required field inside &lt;code&gt;addWatermarkToExcelAction&lt;/code&gt;. Everything else (&lt;code&gt;fontFamily&lt;/code&gt;, &lt;code&gt;fontSize&lt;/code&gt;, &lt;code&gt;fontColor&lt;/code&gt;, &lt;code&gt;semiTransparent&lt;/code&gt;, &lt;code&gt;cultureName&lt;/code&gt;) has a documented default, so a minimal call is genuinely minimal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/remove-watermark/" rel="noopener noreferrer"&gt;Remove Watermark&lt;/a&gt; does the reverse, and worksheet targeting means you can strip a watermark from one tab of a multi-sheet workbook without touching the rest, which matters once a workbook has more than one sheet in it.&lt;/p&gt;

&lt;p&gt;If you're automating this without writing your own integration code, the same operation exists as a native step in &lt;a href="https://docs.pdf4me.com/integration/make/excel/add-text-watermark/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/add-text-watermark/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/add-text-watermark/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/add-text-watermark/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, with removal available the same four ways: &lt;a href="https://docs.pdf4me.com/integration/make/excel/remove-watermark/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/remove-watermark/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/remove-watermark/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/remove-watermark/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A header and footer is a page setup change, and it's still one API call
&lt;/h2&gt;

&lt;p&gt;Worth being precise here: a watermark sits visually behind or across the cell content. A header or footer lives in Excel's page setup, the same strip of space that would show a printed page number if you set it manually by hand. They're different mechanisms solving overlapping problems, which is why branding a workbook often needs both at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/add-text-header-footer/" rel="noopener noreferrer"&gt;Add Text Header/Footer&lt;/a&gt; covers font styling, color, and orientation for text content: page numbers, a "Confidential, internal use only" line that prints on every page rather than sitting over the data. &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/add-image-header-footer/" rel="noopener noreferrer"&gt;Add Image Header/Footer&lt;/a&gt; does the same for a logo file, with positioning, worksheet selection, and margins, so a company logo lands in the same spot on every sheet without anyone dragging an image box around in Excel. &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/remove-header-footer/" rel="noopener noreferrer"&gt;Remove Header/Footer&lt;/a&gt; clears either kind, again with worksheet targeting.&lt;/p&gt;

&lt;p&gt;Same four-platform pattern as watermarking. Text headers and footers: &lt;a href="https://docs.pdf4me.com/integration/make/excel/add-text-header-footer/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/add-text-header-footer/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/add-text-header-footer/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/add-text-header-footer/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;. Image headers and footers: &lt;a href="https://docs.pdf4me.com/integration/make/excel/add-image-header-footer/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/add-image-header-footer/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/add-image-header-footer/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/add-image-header-footer/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;. Removal: &lt;a href="https://docs.pdf4me.com/integration/make/excel/remove-header-footer/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/remove-header-footer/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/remove-header-footer/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/remove-header-footer/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tab nobody meant to send
&lt;/h2&gt;

&lt;p&gt;The most common spreadsheet accident isn't a formatting problem, it's a worksheet problem. A workbook built for internal use has a tab called "Notes" or "Raw Data" or someone's name, and it goes out the door attached to an external email because nobody remembers to delete it before hitting send.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/delete-worksheets/" rel="noopener noreferrer"&gt;Delete Worksheets&lt;/a&gt; removes a sheet by name or index as a normal pipeline step, which means the step that used to depend on a human's memory now depends on nothing at all.&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;monthly-report.xlsx&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;document&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;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;monthly-report.xlsx&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;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;deleteWorksheetToExcelAction&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;worksheetNames&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;Internal Notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="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/office/ApiV2Excel/ExcelDeleteWorksheet&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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can target by &lt;code&gt;worksheetNames&lt;/code&gt; (an array, so multiple tabs in one call) or &lt;code&gt;worksheetIndexes&lt;/code&gt; if the sheet order is more stable than its name in your source system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Extract Worksheets&lt;/a&gt; does the inverse job: pull specific sheets out of a multi-sheet file into their own workbook, by name or index, which is what a "send only the client-facing tab" workflow actually needs under the hood. Chain that per sheet and you get worksheet separation, splitting one workbook into one file per tab, documented as its own integration step in &lt;a href="https://docs.pdf4me.com/integration/make/excel/separate-worksheets/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/separate-worksheets/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/separate-worksheets/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Delete and extract are available across the same four platforms too. Delete: &lt;a href="https://docs.pdf4me.com/integration/make/excel/delete-worksheets/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/delete-worksheets/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/delete-worksheets/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/delete-worksheets/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;. Extract: &lt;a href="https://docs.pdf4me.com/integration/make/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/extract-worksheets/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this belongs in a workflow, not a script someone has to remember to run
&lt;/h2&gt;

&lt;p&gt;A script that runs when a human remembers to run it isn't automation. It's a manual step with extra keystrokes. These operations belong as steps in Make, Zapier, Power Automate, or n8n rather than a standalone script because they attach to a trigger: a file lands in a folder, a form gets submitted, a scheduled time arrives, and the watermarking, branding, and cleanup just happen.&lt;/p&gt;

&lt;p&gt;If you're building your own application instead of wiring together no-code steps, every endpoint above sits behind the same &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;PDF4me V2 REST API&lt;/a&gt;, authenticated the same way, so nothing here is locked to any one platform. Base URL is &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, every request is a POST, and the response comes back as a base64-encoded document in JSON alongside a &lt;code&gt;success&lt;/code&gt; flag and &lt;code&gt;fileName&lt;/code&gt;. That consistency across every Excel endpoint (watermark, header/footer, worksheet) is what makes chaining several of these calls into one pipeline straightforward instead of a new integration each time.&lt;/p&gt;

&lt;p&gt;One honest limit worth stating plainly: this isn't a formula engine, and it isn't a substitute for Excel's own calculation layer. It edits structure, presentation, and branding: watermarks, headers, footers, and which worksheets exist in a file. If the job is recalculating formulas or manipulating cell data at scale, that's a different set of operations (row-level add, update, delete, and merge), worth its own read if that's the problem you're actually solving.&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>productivity</category>
    </item>
    <item>
      <title>Excel as a Database: Updating, Deleting, and Merging Rows Over One API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:29:00 +0000</pubDate>
      <link>https://dev.to/pdf4me/excel-as-a-database-updating-deleting-and-merging-rows-over-one-api-41i5</link>
      <guid>https://dev.to/pdf4me/excel-as-a-database-updating-deleting-and-merging-rows-over-one-api-41i5</guid>
      <description>&lt;p&gt;Most teams don't think of Excel as a database. It's the thing you open before a meeting, not something you'd call from production code. But look at how a lot of internal tools actually work: a workbook sits in a shared drive, a script or a no-code flow reads it, changes a few rows, and writes it back. Nobody built that on purpose. It accumulated, one manual edit at a time, until the spreadsheet became the source of truth nobody wants to admit is the source of truth.&lt;/p&gt;

&lt;p&gt;The problem isn't that Excel is a bad database. It's that the operations you'd actually want (updating a row when a status changes, deleting rows that failed validation, merging duplicate entries after two systems both wrote to the same sheet) still get done by hand in most shops. Someone opens the file, finds the row, edits it, saves it, and hopes nobody else had it open at the same time.&lt;/p&gt;

&lt;p&gt;PDF4me's Excel API turns three of the most common versions of that work, update, delete, merge, into calls you can drop into a pipeline. No macro, no VBA, no "don't touch that cell" tribal knowledge. Here's what each one actually does, the request shape behind it, and where it fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating your requests
&lt;/h2&gt;

&lt;p&gt;Every call below hits the same base URL, &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, under the &lt;code&gt;/api/v2/&lt;/code&gt; path, authenticated with an API key from your &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;PDF4me dashboard&lt;/a&gt;. Full setup is in 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;; the short version is a Basic auth header carrying your Base64-encoded key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating rows without opening the file
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/update-rows/" rel="noopener noreferrer"&gt;Update Rows&lt;/a&gt; endpoint (&lt;code&gt;POST office/ApiV2Excel/ExcelUpdateRows&lt;/code&gt;) takes an existing Excel file and a set of new values, and writes those values into specific rows, targeted either by table structure or by direct coordinates. Coordinate-based targeting is what you want when you know exactly which cell you're touching, row 14, column C. Table-based targeting is what you want when the sheet has headers and you'd rather match by field name than by position, which matters the moment someone inserts a column and every hardcoded coordinate in your script quietly points at the wrong data.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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/api/v2/&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;data.xlsx&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;document&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;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;data.xlsx&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;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;updateRowsToExcelAction&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;jsonInput&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;Name&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;John&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;Age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 31}]&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;worksheetName&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;Sheet1&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;tableName&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;SalesTable&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;excelRowNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;convertNumericAndDate&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;cultureName&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;en-US&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;office/ApiV2Excel/ExcelUpdateRows&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tableName&lt;/code&gt; switches the call into table-based mode; leave it empty and use &lt;code&gt;insertFromRow&lt;/code&gt; / &lt;code&gt;insertFromColumn&lt;/code&gt; instead for coordinate-based mode. The response comes back as a Base64-encoded document alongside &lt;code&gt;fileName&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, and &lt;code&gt;errorMessage&lt;/code&gt; fields, the same shape across all three endpoints below.&lt;/p&gt;

&lt;p&gt;The automation-platform versions add the parts a developer would otherwise have to build by hand. In &lt;a href="https://docs.pdf4me.com/integration/make/excel/update-rows/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, the module does automatic header matching, type conversion, and culture-specific formatting, so a JSON payload from another app lands in the right cell as the right data type instead of as a string that happens to look like a number. &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/update-rows/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; does the same header matching and type conversion, aimed at flows where a form submission or a database row needs to land back in a shared workbook with nobody touching it manually. &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/update-rows/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;'s action modifies existing spreadsheet rows in place from JSON, exactly the shape you'd reach for to keep CRM, inventory, or reporting data synced against a spreadsheet without manual editing. And in &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/update-rows/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, the node matches JSON arrays to worksheet headers while giving you control over the starting row and column offsets, and it preserves the workbook's existing formulas while it refreshes the data around them. That last detail decides whether a self-hosted workflow is safe to run against a sheet someone else already built calculations into.&lt;/p&gt;

&lt;p&gt;Picture the workflow this actually replaces: a support ticket closes, and someone is supposed to open the shared "Active Accounts" workbook finance still swears by, find the account row, and flip a status column from "Open" to "Resolved." Multiply that by however many tickets close in a day, and the manual version is where status columns quietly go stale. Wiring the ticket-close event straight into an Update Rows call means the workbook reflects reality the moment the ticket does, not whenever someone remembers to open it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting rows without breaking the ones after them
&lt;/h2&gt;

&lt;p&gt;Deleting rows sounds simpler than updating them, right up until you've done it wrong once. Delete row 5, and every row after it shifts up by one. Delete rows 5 and 8 in that order, and row 8 was never actually row 8 by the time your script got to it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/delete-rows/" rel="noopener noreferrer"&gt;Delete Rows&lt;/a&gt; endpoint (&lt;code&gt;POST office/ApiV2Excel/ExcelDeleteRows&lt;/code&gt;) removes rows by index range, scoped to a specific worksheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&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;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;data.xlsx&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;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;deleteRowsToExcelAction&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;worksheetName&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;Sheet1&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;fromRow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toRow&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;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;office/ApiV2Excel/ExcelDeleteRows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint, and every automation-platform version, handles the ordering problem the same way: bottom to top. In &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/delete-rows/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, you pass a range syntax like "1-5,10,15-20" and bottom-to-top processing prevents the index-shifting error described above. &lt;a href="https://docs.pdf4me.com/integration/make/excel/delete-rows/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; uses the same comma-separated range syntax with safe bottom-to-top deletion, built for cleanup work: pulling failed rows out of a report, trimming a sheet before it goes to a client, or clearing rows that a retention schedule says shouldn't stick around. &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/delete-rows/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; removes single rows, multiple rows, or comma-separated ranges from any worksheet inside a flow. &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/delete-rows/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;'s version removes single rows, ranges, or mixed lists from any worksheet, without shifting the rows you didn't ask it to touch.&lt;/p&gt;

&lt;p&gt;Don't write your own row-deletion loop unless you enjoy debugging off-by-one errors in someone else's spreadsheet. A nightly QA job that flags failing rows and needs them out of the working sheet before it goes anywhere else is the textbook case: a single Delete Rows call, driven by whatever range your validation step already produced, removes the guesswork along with the rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging duplicate rows without a pivot table
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/merge-rows/" rel="noopener noreferrer"&gt;Merge Rows&lt;/a&gt; endpoint (&lt;code&gt;POST office/ApiV2Excel/ExcelMergeRows&lt;/code&gt;) is really a deduplication tool wearing a merge-rows name. It targets a worksheet or table, groups rows by a key column you choose, and consolidates the values that differ across the group instead of picking one row and discarding the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&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;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;data.xlsx&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;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;mergeRowsToExcelAction&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;jsonInput&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;Name&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;John&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;Age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 30}]&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;worksheetName&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;Sheet1&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;tableName&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;CustomerTable&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;convertNumericAndDate&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;cultureName&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;en-US&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;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;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;office/ApiV2Excel/ExcelMergeRows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That consolidation behavior is where the automation-platform versions get specific. &lt;a href="https://docs.pdf4me.com/integration/make/excel/merge-rows/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;'s implementation groups on key columns and does value consolidation using a semicolon-separated format, then removes the duplicates, built for comprehensive data deduplication across a sheet two systems have both been writing to. &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/merge-rows/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;'s action groups on key columns to combine customer orders, product variants, or category data that landed as separate rows when they should have been one. &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/merge-rows/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt; groups duplicate rows by a key column and combines the differing values with semicolon separators inside the flow. &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/merge-rows/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;'s node groups by key columns, combines values with the same deduplication logic, and exports the result to multiple formats, useful when the merged sheet needs to go somewhere other than back into Excel.&lt;/p&gt;

&lt;p&gt;Merge Rows is the one of the three that most often gets skipped in favor of a manual pivot table and a lot of copy-paste. It's also the one where doing it by hand is most likely to silently drop a value nobody notices missing until a customer asks about it. Two intake forms feeding the same customer list, one row with a phone number and the other with a billing address for the same person, is the scenario this exists for: group on the customer key, let the endpoint consolidate the differing fields, and nobody has to make that judgment call thirty times before lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking REST or a no-code platform, and why it's not really a hard choice
&lt;/h2&gt;

&lt;p&gt;None of this requires picking a side. The REST endpoints and the no-code platform actions do the same underlying work, so the choice comes down to where the trigger for the row change actually lives, not which tool is "better." If you're already inside a codebase, the REST API is the direct path: authenticate once and call the endpoint from wherever your pipeline already runs. If the trigger lives somewhere else, a form submission, a webhook, a scheduled check, that's what &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-make/" rel="noopener noreferrer"&gt;Make&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; connections exist for, so a team without a developer on hand for that one workflow still gets the same row-level behavior through a flow they can read and edit themselves.&lt;/p&gt;

&lt;p&gt;Here's the honest limit: none of these three run inside a single combined call. Update, delete, and merge are three separate requests, chained in whatever order your workflow needs, not one operation that does all three at once. On the REST side, that's three sequential calls, exactly as simple and exactly as much your own responsibility to sequence correctly as it sounds.&lt;/p&gt;

&lt;p&gt;Spreadsheets aren't going away as a business interface. Realistically, they shouldn't. The teams that get the most out of an Excel API like this one aren't the ones who stopped using Excel. They're the ones who stopped treating every row edit as a task for a human.&lt;/p&gt;

&lt;p&gt;Full runnable samples in more languages (C#, Java, Node.js) live in the &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt; repo under &lt;code&gt;Excel/Update Rows&lt;/code&gt;, &lt;code&gt;Excel/Delete Rows&lt;/code&gt;, and &lt;code&gt;Excel/Merge Rows&lt;/code&gt;. New to the API: start with &lt;a href="https://docs.pdf4me.com/general-guidelines/getting-started-api-portal/" rel="noopener noreferrer"&gt;Getting Started with API Portal&lt;/a&gt;.&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>excel</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Your Code Can Know About a PDF Before It Touches a Single Page!!</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:20:44 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-your-code-can-know-about-a-pdf-before-it-touches-a-single-page-ifg</link>
      <guid>https://dev.to/pdf4me/what-your-code-can-know-about-a-pdf-before-it-touches-a-single-page-ifg</guid>
      <description>&lt;p&gt;There are two PDF4me endpoints that sound like they do the same thing: &lt;code&gt;GetPdfInformation&lt;/code&gt; and &lt;code&gt;GetPdfMetadata&lt;/code&gt;. Both take a PDF and hand back facts about it before your code opens it, edits it, or moves it anywhere. The overview version of this piece covers when to reach for each one. This is the version with the actual requests: real field names, a full working script, and one behavior that only shows up once you read the sample code instead of the docs page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same idea, different keys
&lt;/h2&gt;

&lt;p&gt;Here's the part that trips people up. The two endpoints don't even use the same JSON keys for the same data.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST https://api.pdf4me.com/api/v2/GetPdfInformation&lt;/code&gt;&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;"File 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;"document.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"File Content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JVBERi0xLjQK..."&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;POST https://api.pdf4me.com/api/v2/GetPdfMetadata&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JVBERi0xLjQK..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both fields hold the same thing (a filename and the file's Base64 content) but &lt;code&gt;GetPdfInformation&lt;/code&gt; takes &lt;code&gt;File Name&lt;/code&gt; / &lt;code&gt;File Content&lt;/code&gt;, and &lt;code&gt;GetPdfMetadata&lt;/code&gt; takes &lt;code&gt;docContent&lt;/code&gt; / &lt;code&gt;docName&lt;/code&gt;. Copy-pasting a payload from one endpoint to the other silently fails, because the fields it's expecting just aren't there. Worth checking the exact key names for whichever endpoint you're calling instead of assuming they match, even though the docs read like they should.&lt;/p&gt;

&lt;p&gt;Both use the same header pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Type: application/json
Authorization: Basic {api_key}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A working script for GetPdfMetadata
&lt;/h2&gt;

&lt;p&gt;This is adapted from PDF4me's own sample repository (MIT licensed), trimmed down to the request/response logic:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="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/GetPdfMetadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_and_encode_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_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;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="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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_pdf_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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="nf"&gt;read_and_encode_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;docName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Synchronous: metadata comes back in the response body
&lt;/span&gt;        &lt;span class="k"&gt;return&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;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;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Asynchronous: poll the Location header until it's 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;10&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;return&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="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;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Metadata extraction did not complete in time&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="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;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pdf_metadata&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two response codes to handle: &lt;code&gt;200&lt;/code&gt; returns the metadata JSON directly, &lt;code&gt;202&lt;/code&gt; means it's processing async and hands you a &lt;code&gt;Location&lt;/code&gt; header to poll. The official sample polls up to 10 times, 10 seconds apart, which is a reasonable starting point for anything that isn't a batch job.&lt;/p&gt;

&lt;h2&gt;
  
  
  GetPdfInformation, same pattern, smaller payload
&lt;/h2&gt;

&lt;p&gt;There's no official Python sample published for this endpoint yet (only a C# one, in the same repo), but it follows the identical auth and encoding pattern, just with its own field names and no documented &lt;code&gt;async&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/api/v2/GetPdfInformation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_pdf_information&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_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;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;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;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="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;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;document.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;File Content&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="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="k"&gt;return&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;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pdf_information&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response here is flatter too: a single &lt;code&gt;Metadata Info&lt;/code&gt; field rather than the structured document/file/security breakdown &lt;code&gt;GetPdfMetadata&lt;/code&gt; returns. If your triage logic just needs author, title, and encryption status, this is the lighter call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The async flag the docs page doesn't mention
&lt;/h2&gt;

&lt;p&gt;This is the part worth flagging for anyone who read the marketing docs page and stopped there. &lt;code&gt;docs.pdf4me.com&lt;/code&gt;'s own request example for &lt;code&gt;GetPdfMetadata&lt;/code&gt; shows a two-field payload, no &lt;code&gt;async&lt;/code&gt; flag anywhere. But PDF4me's official sample script sends &lt;code&gt;"async": true&lt;/code&gt; by default and handles a &lt;code&gt;202 Accepted&lt;/code&gt; response with polling. That behavior exists at the REST level, it's just not visible unless you go read the sample code instead of the parameter table.&lt;/p&gt;

&lt;p&gt;Practically: for a single small file, the endpoint can return &lt;code&gt;200&lt;/code&gt; with the metadata inline. For larger files, or under load, expect a &lt;code&gt;202&lt;/code&gt; and a &lt;code&gt;Location&lt;/code&gt; header to poll. Build for both from the start rather than assuming a hardcoded &lt;code&gt;200&lt;/code&gt; handler covers production traffic.&lt;/p&gt;

&lt;p&gt;That also puts n8n's &lt;code&gt;GetPdfMetadata&lt;/code&gt; node in a different light. Its Async toggle isn't just an n8n-side convenience, it's very likely surfacing this exact REST-level &lt;code&gt;async&lt;/code&gt; parameter. That's an inference from matching behavior, not something n8n's own docs state outright, so treat it as a reasonable working assumption rather than a confirmed 1:1 mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this shows up outside a raw HTTP client
&lt;/h2&gt;

&lt;p&gt;Power Automate is the only platform with both actions as separate, named steps, each taking the same File Name / File Content pair, mappable straight from SharePoint, OneDrive, or an email trigger. Zapier ships a &lt;code&gt;GetPdfMetadata&lt;/code&gt; action that takes a File and File Name. n8n's &lt;code&gt;GetPdfMetadata&lt;/code&gt; node adds the input-format choice (binary, Base64, or URL) plus the Async toggle covered above.&lt;/p&gt;

&lt;p&gt;There's no Make module for either endpoint yet, and no dedicated Zapier or n8n page for &lt;code&gt;GetPdfInformation&lt;/code&gt; specifically. If you're on Make, or need &lt;code&gt;GetPdfInformation&lt;/code&gt; inside Zapier or n8n today, the REST call above is the way in.&lt;/p&gt;

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

&lt;p&gt;Match the endpoint to the question. "Who made this file, and is it locked?" is &lt;code&gt;GetPdfInformation&lt;/code&gt;. "Does this meet an archival, size, or signature standard, and how many pages is it?" is &lt;code&gt;GetPdfMetadata&lt;/code&gt;. Run the cheaper call where you can, and build the &lt;code&gt;202&lt;/code&gt;/polling path for &lt;code&gt;GetPdfMetadata&lt;/code&gt; from day one rather than retrofitting it after the first large file times out in production.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(A more editorial, less code-heavy version of this piece is also up on Medium and Hashnode.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;p&gt;Developer portal: &lt;a href="https://dev.pdf4me.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=dev-c1-dw01&amp;amp;utm_content=learn-more-devportal" rel="noopener noreferrer"&gt;dev.pdf4me.com&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://docs.pdf4me.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=dev-c1-dw01&amp;amp;utm_content=learn-more-docs" rel="noopener noreferrer"&gt;docs.pdf4me.com&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://pdf4me.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=dev-c1-dw01&amp;amp;utm_content=learn-more-website" rel="noopener noreferrer"&gt;pdf4me.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
