<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: PDF4me</title>
    <description>The latest articles on DEV Community by PDF4me (@pdf4me).</description>
    <link>https://dev.to/pdf4me</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4030343%2Fb2bd2942-2a26-4c0d-9fcf-bc47b6b17f04.png</url>
      <title>DEV Community: PDF4me</title>
      <link>https://dev.to/pdf4me</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pdf4me"/>
    <language>en</language>
    <item>
      <title>How to Remove a Password From a Protected Excel File via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:59:08 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-remove-a-password-from-a-protected-excel-file-via-api-15cn</link>
      <guid>https://dev.to/pdf4me/how-to-remove-a-password-from-a-protected-excel-file-via-api-15cn</guid>
      <description>&lt;h1&gt;
  
  
  How to Remove a Password From a Protected Excel File via API
&lt;/h1&gt;

&lt;p&gt;Somebody in finance sends you a nightly export. It's encrypted, because of course it is, their IT policy requires it, and the password lives in a shared vault your automation already has access to. Your pipeline grabs the file, and then just sits there, because every downstream step you've built, reading rows, merging workbooks, extracting a worksheet, expects a plain, unprotected &lt;code&gt;.xlsx&lt;/code&gt;. A password-protected file isn't a smaller version of the same problem. It's a different file format as far as your code is concerned, and the first thing any automation touching it has to do is take the lock off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a protected workbook breaks more than you'd expect
&lt;/h2&gt;

&lt;p&gt;Excel password protection isn't one setting. A workbook can be locked at the file level, so it needs a password just to open at all, at the workbook level, so nobody can add, delete, rename, or reorder sheets even after opening it, and at the worksheet level, so individual cells and formulas on a specific tab can't be edited. Those three locks are independent. A file can have all three, just one, or any combination, and a script that only knows how to strip file-open encryption will still choke on a workbook whose structure or worksheets are separately locked.&lt;/p&gt;

&lt;p&gt;That's the actual shape of the problem &lt;a href="https://pdf4me.com/office-api" rel="noopener noreferrer"&gt;PDF4me's Office API&lt;/a&gt; solves for Excel. It handles Word and Excel automation over HTTPS, no Microsoft Office installation required on either end, and the security side of that API covers both directions: locking a workbook down with &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/secure-document/" rel="noopener noreferrer"&gt;Secure Excel&lt;/a&gt;, and reversing it with the operation this article is about, Unlock Excel.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/unlock-document/" rel="noopener noreferrer"&gt;Unlock Excel REST endpoint&lt;/a&gt; is a single &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;https://api.pdf4me.com/office/ApiV2Excel/ExcelUnlock&lt;/code&gt;, with &lt;code&gt;Content-Type: application/json&lt;/code&gt; and a Basic &lt;code&gt;Authorization&lt;/code&gt; header carrying your API key.&lt;/p&gt;

&lt;p&gt;The request body has three parts: a &lt;code&gt;document&lt;/code&gt; object naming the file (a &lt;code&gt;Name&lt;/code&gt; field, like &lt;code&gt;data.xlsx&lt;/code&gt;), a top-level &lt;code&gt;docContent&lt;/code&gt; string holding the whole workbook as Base64, and an &lt;code&gt;unlockExcelAction&lt;/code&gt; object holding the one field that actually matters, &lt;code&gt;password&lt;/code&gt;, plus an optional &lt;code&gt;cultureName&lt;/code&gt; for locale-sensitive processing. Here's what that looks like end to end in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF4ME_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;EXCEL_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXCEL_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# never hardcode this
&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;unlockExcelAction&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;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EXCEL_PASSWORD&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/office/ApiV2Excel/ExcelUnlock&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;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;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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&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;success&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="c1"&gt;# Wrong password, or any other failure, lands here.
&lt;/span&gt;    &lt;span class="c1"&gt;# The file is returned untouched -- nothing partial happens.
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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="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;errorMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unlock failed, no error message returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;unlocked_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;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;fileName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_unlocked.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;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;unlocked_bytes&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 hands back the unprotected workbook as a Base64 &lt;code&gt;document&lt;/code&gt;, a &lt;code&gt;fileName&lt;/code&gt;, a &lt;code&gt;success&lt;/code&gt; flag, and an &lt;code&gt;errorMessage&lt;/code&gt; that gets populated specifically when the password is wrong. That last point is worth sitting with for a second: this is explicitly described as requiring the correct password, not cracking one. Feed it the wrong password and you get &lt;code&gt;success: false&lt;/code&gt; and an untouched source file back, not a brute-force attempt and not a partially unlocked result. If you don't have the password, this endpoint has nothing for you. That's a deliberate boundary, not a missing feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  One password field on REST, three on every integration platform
&lt;/h2&gt;

&lt;p&gt;Here's the detail worth knowing before you build on more than one surface, because it isn't obvious from the REST page alone. The REST endpoint takes exactly one &lt;code&gt;password&lt;/code&gt; field, which only works cleanly if a workbook's file-open password, workbook-structure password, and worksheet password are all the same value, or if the file only has one of those three locks to begin with.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/unlock-excel/" rel="noopener noreferrer"&gt;Power Automate action&lt;/a&gt;, the &lt;a href="https://docs.pdf4me.com/integration/make/excel/unlock-excel/" rel="noopener noreferrer"&gt;Make module&lt;/a&gt;, and the &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/unlock-excel/" rel="noopener noreferrer"&gt;Zapier action&lt;/a&gt; all expose three separate, independently optional password fields instead of one: a file-open password, a workbook-structure password, and a worksheet password. All three apply a fallback chain: supply just the file-open password, and each platform tries it against the workbook and worksheet locks too before giving up. That's a meaningfully more capable interface than the bare REST call for the common real-world case of a file where IT set one password for everything, but it also means a workbook locked with genuinely different passwords at each layer needs the right one supplied at the right layer, on every platform except REST, which simply can't express that case at all with a single field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/unlock-excel/" rel="noopener noreferrer"&gt;PDF4me's n8n node&lt;/a&gt; handles the same three layers slightly differently again: it keeps separate File Encryption and Workbook Protection password fields, but worksheet-level unlocking is a plain on/off toggle, "Unlock Worksheets," off by default, rather than its own password input. Three platforms, three different takes on the same three-layer lock. Worth confirming against your actual file before you assume any one platform's shape, especially if you're porting automation logic from one of these to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this operation won't do, and its direct inverse
&lt;/h2&gt;

&lt;p&gt;Unlock Excel is strictly a removal operation. It doesn't inspect a file to tell you which of the three lock types are present before you call it, it doesn't recover a forgotten password, and it doesn't touch anything else about the workbook: formulas, formatting, sheet contents, and cell values all come back exactly as they went in. If what you actually need is to add protection in the first place, not remove it, that's the separate &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/secure-document/" rel="noopener noreferrer"&gt;Secure Excel endpoint&lt;/a&gt;, which takes the inverse set of options: a password, a &lt;code&gt;protectWorkbook&lt;/code&gt; flag, a &lt;code&gt;protectWorksheets&lt;/code&gt; flag with a &lt;code&gt;worksheetProtectionType&lt;/code&gt; of Contents, Objects, Scenarios, or All, and independent fallback passwords for each layer, mirroring the same three-lock model Unlock Excel removes. If your pipeline both receives protected files from one system and needs to re-protect its own output before handing it to another, you're looking at these two endpoints as a matched pair, not two unrelated features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which surface to build this on
&lt;/h2&gt;

&lt;p&gt;None of the four integration platforms change what comes back, only how much control you get over which password goes where, so the choice mostly comes down to where the protected file already lands. If it arrives through a shared mailbox, a SharePoint library, or anywhere else already inside Microsoft 365, Power Automate keeps the whole unlock step inside that ecosystem without adding a second tool to maintain. Make's own documentation describes pulling the file in as binary content mapped from a prior step, such as a cloud-storage download, which fits a scenario where the protected workbook is already sitting in Drive, Dropbox, or an FTP drop before your scenario ever touches it. Zapier is the natural fit when the protected file is already the output of an earlier Zap step, a form upload or an inbox attachment parser, since the Unlock action just slots in as the next step with nothing extra to configure. n8n suits teams already running self-hosted or hybrid automation who want the unlock step living in the same workflow as everything else, including, in this one case, a plain on/off switch for the worksheet layer instead of a password field to fill in. REST is the fit when you want the logic living in your own codebase instead of a third-party automation platform, as in the Python example above.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic version of this: the nightly encrypted export
&lt;/h2&gt;

&lt;p&gt;Picture a payroll or banking integration that drops a password-protected &lt;code&gt;.xlsx&lt;/code&gt; into a monitored folder every night, the password itself stored once in a secrets manager rather than typed by a human each time. A workflow built on &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/unlock-excel/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/excel/unlock-excel/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/unlock-excel/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/unlock-excel/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, or a scheduled script like the one above picks the file up on arrival, pulls the password from that secrets store, and runs the Unlock action before anything else touches the file. Every step after that, extracting specific rows, merging the sheet into a master report, reading values into a database, operates on a workbook that's never been manually opened by a person at all. The lock that used to mean "someone has to open this by hand first" becomes a single step at the front of the pipeline instead, and because a wrong password fails loudly with &lt;code&gt;errorMessage&lt;/code&gt; populated rather than silently returning garbage, a bad credential in the secrets store gets caught the same night, not three reports downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;If you're calling this directly, start with the &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/unlock-document/" rel="noopener noreferrer"&gt;REST API reference&lt;/a&gt;, and remember that it assumes one password covers every lock on the file. If you're building inside a no-code tool and need to handle a file-open password that differs from its workbook or worksheet passwords, the &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/unlock-excel/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/excel/unlock-excel/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/unlock-excel/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/unlock-excel/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; actions give you that flexibility, each with its own small variation on how the worksheet layer is addressed. And if your workflow needs to protect a file before Unlock Excel ever needs to touch it again, &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/secure-document/" rel="noopener noreferrer"&gt;Secure Excel&lt;/a&gt; is the other half of that pair. Either way, what used to require a person with the password sitting at a keyboard becomes one HTTP call that either returns a clean, workable file or tells you plainly, and immediately, that the password was wrong.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Password-Protect an Excel Workbook and Individual Worksheets via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:34:17 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-password-protect-an-excel-workbook-and-individual-worksheets-via-api-1kb0</link>
      <guid>https://dev.to/pdf4me/how-to-password-protect-an-excel-workbook-and-individual-worksheets-via-api-1kb0</guid>
      <description>&lt;h1&gt;
  
  
  How to Password-Protect an Excel Workbook and Individual Worksheets via API
&lt;/h1&gt;

&lt;p&gt;Somebody on your finance team sends out a pricing workbook. Three reviewers get a copy. One of them "just tidies up a formula," another deletes a sheet they didn't think anyone needed, and by the time the file comes back the totals don't match what went out. Nobody did anything malicious. They just had a spreadsheet, and a spreadsheet with no protection on it is an invitation to edit.&lt;/p&gt;

&lt;p&gt;Excel actually gives you three separate locks for exactly this problem, and most developers only ever reach for one of them, if any. &lt;a href="https://pdf4me.com/office-api" rel="noopener noreferrer"&gt;PDF4me's Office API&lt;/a&gt; exposes all three through a single call: &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/secure-document/" rel="noopener noreferrer"&gt;Secure Document&lt;/a&gt; for Excel. Here's what each lock actually does, how to combine them, and where the real limits are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three locks, not one
&lt;/h2&gt;

&lt;p&gt;It helps to stop thinking of "password-protecting Excel" as one feature. It's three, and they answer different questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File encryption&lt;/strong&gt; answers "can this person even open the file?" Set a password here and the workbook won't open at all without it. This is the lock most people mean when they say "password protect."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workbook structure protection&lt;/strong&gt; answers "can someone rearrange the file itself?" This is the lock that stops a sheet from being added, deleted, renamed, hidden, or reordered. It has nothing to do with the data inside any one sheet. It protects the container, not the contents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worksheet content protection&lt;/strong&gt; answers "can someone edit what's actually in the cells?" This is the familiar "unprotect sheet" dialog in Excel, except you're setting it programmatically, at scale, on files that were never touched by a human in Excel's own UI.&lt;/p&gt;

&lt;p&gt;The reason the pricing workbook above got mangled is that whoever built the original file probably set none of these, or set file encryption and assumed that was the whole job. It wasn't. A password to open a file does nothing to stop someone who already has it open from renaming "Q3 Actuals" to "Q3 Actuals (old)" and starting fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one call that sets all three
&lt;/h2&gt;

&lt;p&gt;PDF4me's &lt;code&gt;ExcelSecure&lt;/code&gt; endpoint takes a document and a &lt;code&gt;secureExcelAction&lt;/code&gt; object, and that object is where all three locks live together. Here's the exact request shape, verified 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/office/ApiV2Excel/ExcelSecure
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 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;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pricing-workbook.xlsx"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"secureExcelAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openme123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protectWorkbook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protectWorkbookPassword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"structure456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protectWorksheets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetProtectionPassword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cells789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetProtectionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rates,Config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetIndexes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="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;"cultureName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking down what each field in &lt;code&gt;secureExcelAction&lt;/code&gt; actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;password&lt;/code&gt;: encrypts the file so it has to be entered just to open it. Leave the other two passwords blank and this one quietly becomes their fallback too, which matters more than it sounds like it should (see below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;protectWorkbook&lt;/code&gt; (a boolean, default &lt;code&gt;false&lt;/code&gt;) plus &lt;code&gt;protectWorkbookPassword&lt;/code&gt;: locks the structure. Someone with the workbook password can still open the file and read every sheet. They just can't touch the sheet list itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;protectWorksheets&lt;/code&gt; (boolean, default &lt;code&gt;false&lt;/code&gt;) plus &lt;code&gt;worksheetProtectionPassword&lt;/code&gt; and &lt;code&gt;worksheetProtectionType&lt;/code&gt;: locks cell-level editing. &lt;code&gt;worksheetProtectionType&lt;/code&gt; takes &lt;code&gt;Contents&lt;/code&gt;, &lt;code&gt;Objects&lt;/code&gt;, &lt;code&gt;Scenarios&lt;/code&gt;, or &lt;code&gt;All&lt;/code&gt;, and defaults to &lt;code&gt;All&lt;/code&gt; when you don't set it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;worksheetNames&lt;/code&gt; or &lt;code&gt;worksheetIndexes&lt;/code&gt;: a comma-separated list that decides which sheets the worksheet lock actually applies to. Leave both empty and every sheet in the workbook gets protected. &lt;code&gt;worksheetIndexes&lt;/code&gt; is 0-based, so the first sheet is index &lt;code&gt;0&lt;/code&gt;, not &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cultureName&lt;/code&gt;: a culture code (&lt;code&gt;en-US&lt;/code&gt; and similar) for anything locale-sensitive in how the file gets processed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response comes back with the protected file, also Base64, plus a filename, a success flag, and an error message if something went wrong:&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;"document"&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;"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;"pricing-workbook.xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;One call, three layers, done. No separate round trip for each lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the password fallback matters more than it looks
&lt;/h2&gt;

&lt;p&gt;Here's the detail that trips people up: if you set &lt;code&gt;password&lt;/code&gt; but leave &lt;code&gt;protectWorkbookPassword&lt;/code&gt; and &lt;code&gt;worksheetProtectionPassword&lt;/code&gt; empty, PDF4me doesn't leave those two layers unprotected. It uses &lt;code&gt;password&lt;/code&gt; for them too. That's a sensible default if your intent really is "just lock the whole thing with one password," but it means you can't assume an empty password field means "no protection at this level." Check whether &lt;code&gt;password&lt;/code&gt; is set before you draw that conclusion, or you'll end up debugging a file that's actually locked in ways you didn't expect.&lt;/p&gt;

&lt;p&gt;The flip side is just as useful in practice. Set all three passwords independently and you can hand one reviewer the file-open password and a completely different, separate password for editing the protected sheets. That covers a surprising amount of the "share this internally but keep it locked down" requests that show up in finance and ops workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting some sheets and not others
&lt;/h2&gt;

&lt;p&gt;Most real workbooks mix sheets that should never change with sheets that are meant to be filled in. Think a locked "Rates" or "Config" tab sitting next to an open "Input" tab that a user fills out every week. That's exactly what &lt;code&gt;worksheetNames&lt;/code&gt; and &lt;code&gt;worksheetIndexes&lt;/code&gt; are for: name the sheets you want locked, by name or by position, and everything else in the file stays fully editable.&lt;/p&gt;

&lt;p&gt;A practical pattern: protect every sheet except the one input tab by naming everything else explicitly, or flip it around and protect by index if your sheet names tend to change between files but the underlying layout doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same call, four different front doors
&lt;/h2&gt;

&lt;p&gt;If you're not calling the REST endpoint directly, the exact same settings show up in whichever automation tool you already run on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/secure-excel/" rel="noopener noreferrer"&gt;Secure Excel in Power Automate&lt;/a&gt; exposes the same password, workbook, and worksheet fields as a flow action, with outputs for success and an errors array if something fails partway through.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/excel/secure-excel/" rel="noopener noreferrer"&gt;Secure Excel for Make&lt;/a&gt; wires into a scenario the same way any other PDF4me module does: map a file name and document from an earlier step, set your passwords and protection toggles, and pass the resulting protected file on to an upload, email, or archive step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/secure-excel/" rel="noopener noreferrer"&gt;Secure Excel for n8n&lt;/a&gt; accepts the input file as binary data, a Base64 string, or a public URL, which is handy if the workbook you're protecting is already sitting at a URL rather than attached to a node in your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/excel/secure-excel/" rel="noopener noreferrer"&gt;Secure Excel for Zapier&lt;/a&gt; needs only the file itself as a required field, with everything else, including the password, optional, which makes it the fastest one to wire up for a simple one-password use case.&lt;/p&gt;

&lt;p&gt;Whichever one you use, you're setting the same &lt;code&gt;password&lt;/code&gt; / &lt;code&gt;protectWorkbook&lt;/code&gt; / &lt;code&gt;protectWorksheets&lt;/code&gt; triangle described above. Learning it once on the platform you already automate with carries over everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking the lock back off
&lt;/h2&gt;

&lt;p&gt;Protection that can never come off isn't protection, it's a dead end. When you need to reverse it, the same four-platform pattern exists for &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/unlock-document/" rel="noopener noreferrer"&gt;unlocking an Excel file&lt;/a&gt;: send the protected document and the password that was used to lock it, and the &lt;code&gt;ExcelUnlock&lt;/code&gt; endpoint strips file encryption, workbook structure protection, and worksheet locks in a single pass.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what this does and doesn't do. It doesn't recover a password you've lost, and there's no way around that by design. It removes protection you already hold the key to, which is the only version of "unlock" you'd actually want an API offering in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this protection is actually for
&lt;/h2&gt;

&lt;p&gt;Worth saying plainly, because it's easy to assume otherwise: Excel's built-in protection, whether you set it by hand or through an API, is not the same thing as encrypting a file against someone who's determined to get in. PDF4me's own documentation for Secure Excel says it directly: standard Excel password protection deters casual access, it isn't strong cryptographic security.&lt;/p&gt;

&lt;p&gt;Treat it as what it's actually good at: stopping a well-meaning colleague from reorganizing your sheet layout or typo-ing over a formula. Don't treat it as a substitute for proper document encryption if what you're actually worried about is a determined attacker.&lt;/p&gt;

&lt;p&gt;That distinction decides what you reach for. Sending a budget template to twelve regional managers who shouldn't rename each other's tabs? Workbook and worksheet protection is exactly the right tool. Sending something that actually needs to resist a motivated outsider? That's a different problem entirely, and treating Excel's protection as if it solves it is the mistake worth avoiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in a bigger pipeline
&lt;/h2&gt;

&lt;p&gt;The real value here isn't locking one file by hand. It's that &lt;code&gt;ExcelSecure&lt;/code&gt; becomes a normal, invisible step in a pipeline you already have running. Generate a report, merge in the latest rows, then secure it before it goes out the door, automatically, for every file, every time, with nobody needing to remember to click "Protect Sheet" before hitting send.&lt;/p&gt;

&lt;p&gt;That's the real difference between a security setting and a security guarantee. One depends on a person remembering to do it correctly every single time. The other doesn't depend on anyone at all, which is the whole point of putting it behind an API in the first place.&lt;/p&gt;

&lt;p&gt;If you can &lt;a href="https://docs.pdf4me.com/url-api-tester" rel="noopener noreferrer"&gt;test the endpoint interactively&lt;/a&gt; before wiring it into a flow, do that first, so you know exactly what the response looks like before anything depends on it. Then build it in once, and every workbook that passes through your pipeline gets the same three locks, every time, without anyone having to think about it again.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Pull Specific Rows or Ranges Out of an Excel Workbook as Structured Data</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:54:52 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-pull-specific-rows-or-ranges-out-of-an-excel-workbook-as-structured-data-2nk8</link>
      <guid>https://dev.to/pdf4me/how-to-pull-specific-rows-or-ranges-out-of-an-excel-workbook-as-structured-data-2nk8</guid>
      <description>&lt;h1&gt;
  
  
  How to Pull Specific Rows or Ranges Out of an Excel Workbook as Structured Data
&lt;/h1&gt;

&lt;p&gt;Most of the time, nobody wants your whole spreadsheet. A finance team doesn't need the formatting, the pivot tables, or the three dead sheets nobody deleted. They want rows 2 through 500 of the "Transactions" tab, as JSON, so their own system can do something with it. That gap between "here is an Excel file" and "here is the data I actually need" is where a surprising amount of integration code gets written badly, because reading a specific slice of a worksheet programmatically is less trivial than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reading a worksheet is harder than opening it
&lt;/h2&gt;

&lt;p&gt;Open Excel, select a range, copy it. Done, for a human. For code, that same task means parsing a binary or XML-based file format, resolving which sheet you mean when a workbook has twelve of them, deciding whether row one is a header or data, and handling the fact that spreadsheets are full of things that break naive row-counting: hidden rows, fully blank rows left over from a deleted filter, and cells that are visually empty but not actually &lt;code&gt;null&lt;/code&gt;. None of that is specific to PDF4me. It is just what "extract rows from Excel" means once you take it seriously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pdf4me.com/office-api" rel="noopener noreferrer"&gt;PDF4me's Office API&lt;/a&gt; handles Word and Excel automation over HTTPS, no Microsoft Office installation required on either end. For Excel specifically, that includes adding rows, updating rows in place, merging workbooks, securing and unlocking files, and the operation this article is about: extracting rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Extract Rows actually does
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-rows/" rel="noopener noreferrer"&gt;Extract Rows REST endpoint&lt;/a&gt; is deliberately narrow. It is a read-only call. Send it a workbook, and it hands back a slice of that workbook as structured JSON. It does not modify, resave, or return a new file.&lt;/p&gt;

&lt;p&gt;You &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;https://api.pdf4me.com/office/ApiV2Excel/ExcelExtractRows&lt;/code&gt; with a &lt;code&gt;Content-Type: application/json&lt;/code&gt; header and an &lt;code&gt;Authorization: Basic&lt;/code&gt; header. The body carries the workbook's Base64-encoded content plus an &lt;code&gt;extractRowsToExcelAction&lt;/code&gt; object. Here's the minimal shape, straight from PDF4me's own docs:&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;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data.xlsx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UEsDBBQABgAIAAAA..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extractRowsToExcelAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;An empty &lt;code&gt;extractRowsToExcelAction&lt;/code&gt; reads the default sheet in full. More usefully, it takes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;worksheetName&lt;/code&gt; or &lt;code&gt;worksheetIndex&lt;/code&gt;, to target a specific sheet by name or by its zero-based position.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fromRow&lt;/code&gt; and &lt;code&gt;toRow&lt;/code&gt;, the row range you want, also zero-based, where &lt;code&gt;-1&lt;/code&gt; means "through the last row that actually has data."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cultureName&lt;/code&gt;, which controls how dates and numbers get serialized. A workbook built in Germany and read with &lt;code&gt;en-US&lt;/code&gt; culture can silently turn &lt;code&gt;1.234,56&lt;/code&gt; into the wrong number, so this field matters more than it looks like it should.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filled in, pulling a named sheet with the first 100 rows:&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;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sales.xlsx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UEsDBBQABgAIAAAA..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extractRowsToExcelAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"worksheetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Q3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fromRow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"toRow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cultureName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response comes back as a &lt;code&gt;rowData&lt;/code&gt; array, one object per row, plus a &lt;code&gt;success&lt;/code&gt; flag you should check before you touch &lt;code&gt;rowData&lt;/code&gt; at all, and an &lt;code&gt;errorMessage&lt;/code&gt; that gets populated on failure, things like a missing worksheet or a corrupted file:&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;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rowData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ColumnA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ColumnB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ColumnA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ColumnB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value4"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Row indexing being zero-based is a classic source of off-by-one bugs: "give me the first 100 rows" is &lt;code&gt;fromRow: 0, toRow: 99&lt;/code&gt;, not &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the same call in Python, using the request shape above:&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;sales.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;sales.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;extractRowsToExcelAction&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;Q3&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;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;99&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pdf4me.com/office/ApiV2Excel/ExcelExtractRows&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;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="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;if&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;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&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;rowData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;errorMessage&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;h2&gt;
  
  
  Headers, hidden rows, and the options the REST endpoint keeps minimal
&lt;/h2&gt;

&lt;p&gt;The bare REST action is intentionally thin. The richer control over what counts as a "row" lives in PDF4me's integration-platform actions for this same operation, and they're worth knowing about even if you're calling the REST endpoint directly, because they show you what edge cases exist. The &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/extract-rows/" rel="noopener noreferrer"&gt;Power Automate action&lt;/a&gt; and the &lt;a href="https://docs.pdf4me.com/integration/make/excel/extract-rows/" rel="noopener noreferrer"&gt;Make module&lt;/a&gt; both expose: treating the first row as column headers (or auto-naming columns &lt;code&gt;Column1&lt;/code&gt;, &lt;code&gt;Column2&lt;/code&gt;...), excluding hidden rows and hidden columns, excluding fully empty rows, and choosing whether to export every value as text or keep native types like numbers, booleans, and dates. That last option matters if you're piping the output straight into a system that's picky about type, a date that arrives as a string versus an ISO timestamp is a different bug report depending on which side of the integration you're debugging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/extract-rows/" rel="noopener noreferrer"&gt;PDF4me's n8n action&lt;/a&gt; adds one more convenience worth calling out: it accepts the source file as binary data, a Base64 string, or a public URL, and writes its result, including a &lt;code&gt;rowsExtracted&lt;/code&gt; count and the full settings it applied, into a binary slot your next node can read directly via &lt;code&gt;$json.extractedData.rowData&lt;/code&gt;. That's a smaller detail, but it's the difference between wiring three nodes together cleanly and writing a Function node to reshape the output first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one inconsistency worth knowing about before you build on more than one platform
&lt;/h2&gt;

&lt;p&gt;Here's something the docs don't call out directly, so it's worth being explicit: &lt;strong&gt;row and column numbering is not consistent across every PDF4me Extract Rows surface.&lt;/strong&gt; The REST endpoint, the Power Automate action, the Make module, and the n8n node all use zero-based indexing, where &lt;code&gt;-1&lt;/code&gt; reaches the last row or column with data. &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/extract-rows/" rel="noopener noreferrer"&gt;PDF4me's Zapier action&lt;/a&gt; is the odd one out: its First Row, Last Row, First Column, and Last Column fields are explicitly documented as 1-based, "matching Excel," with &lt;code&gt;-1&lt;/code&gt; still meaning "through the last row or column with data." If you're moving the same automation logic from, say, Make to Zapier, that difference will cost you an off-by-one bug the first time you port a hardcoded range instead of re-deriving it. Treat that number as something to re-verify per platform, not something to copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this operation will not do for you
&lt;/h2&gt;

&lt;p&gt;Extract Rows scopes by position: a starting row, an ending row, optionally a worksheet. None of the four platforms describe any way to filter by a cell's actual value, "give me only the rows where Status equals Overdue" is not something this endpoint does. If you need that, you pull the full range and filter in your own code or your automation platform's own filter step, treat Extract Rows as the data-fetching half of the job, not the querying half.&lt;/p&gt;

&lt;p&gt;It's also worth separating this from two adjacent operations you'll bump into in the same product area. &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-worksheets/" rel="noopener noreferrer"&gt;Extract Worksheets&lt;/a&gt; works one level up: instead of pulling specific rows from a sheet, it returns new workbooks containing only the whole tabs you select, by name or index. If what you actually want is "give me just the 'Q3' tab as its own file," that's Extract Worksheets, not Extract Rows. And if the workbook you're trying to read is password-protected, Extract Rows will simply fail against it. &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/unlock-document/" rel="noopener noreferrer"&gt;PDF4me's Unlock Document action&lt;/a&gt; removes file encryption and worksheet protection first, given the correct password. It can't recover a password you don't have, so this is a decryption step, not a bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic version of this: nightly report to API feed
&lt;/h2&gt;

&lt;p&gt;Picture a finance system that drops a fresh &lt;code&gt;.xlsx&lt;/code&gt; export into SharePoint or Google Drive every night at 2 a.m., a transaction log with a header row and anywhere from 40 to 4,000 data rows depending on the day. Manually opening that file to copy data into another system doesn't scale past about day three. Wired through Power Automate, Make, or n8n, the shape of the automation looks the same regardless of which platform you pick: a trigger fires when the new file lands, the Extract Rows action reads it with headers enabled and hidden rows excluded, and the resulting &lt;code&gt;RowData&lt;/code&gt; array gets looped, with each row posted to an internal REST API or queued into a database insert. The entire "parse last night's spreadsheet" problem collapses into one action step, instead of a scheduled script somebody has to maintain and restart when it silently stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;If you're calling this directly, start with the &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/extract-rows/" rel="noopener noreferrer"&gt;REST API reference&lt;/a&gt; and get the zero-based range math right before you add platform-specific options on top. If you're building this inside a no-code tool, the &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/extract-rows/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/excel/extract-rows/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/extract-rows/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/extract-rows/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; actions each wrap the same underlying call, with the row-numbering caveat above being the one detail that doesn't travel automatically between them. Either way, a worksheet that used to require someone opening Excel by hand becomes one HTTP call that returns exactly the rows you asked for and nothing else.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>nocode</category>
    </item>
    <item>
      <title>How to Append Data to an Excel Worksheet via API Without Opening Excel</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 08 Oct 2026 19:26:40 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-append-data-to-an-excel-worksheet-via-api-without-opening-excel-35hl</link>
      <guid>https://dev.to/pdf4me/how-to-append-data-to-an-excel-worksheet-via-api-without-opening-excel-35hl</guid>
      <description>&lt;h1&gt;
  
  
  How to Append Data to an Excel Worksheet via API Without Opening Excel
&lt;/h1&gt;

&lt;p&gt;Every business has at least one spreadsheet that quietly runs half of it. A "DailySales" workbook that finance refreshes every morning. A response log that catches every form submission before anyone builds a real database for it. A CRM export that gets emailed around every Monday because nobody trusts the dashboard. These workbooks work, right up until someone has to update them without actually opening Excel: a scheduled job, a webhook, a background service that needs to write five new rows into a worksheet that a human is going to open two hours later.&lt;/p&gt;

&lt;p&gt;That's a smaller problem than it sounds, and also a bigger one. Smaller, because appending rows to an Excel file is conceptually trivial: take some data, put it below the last row. Bigger, because the moment you actually write that code yourself, you run into questions a one-line "append a row" ask never mentions. Does the new data need to match an existing header row, or go into specific cells? What happens if the sheet is a real Excel Table, with named columns and formatting that breaks if you write past its boundary? Does a date string need to become an actual Excel date, or does it arrive as plain text? And what happens when the thing that needs those three new rows is a serverless function that has no business knowing what &lt;code&gt;openpyxl&lt;/code&gt; or &lt;code&gt;ClosedXML&lt;/code&gt; is?&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/add-rows/" rel="noopener noreferrer"&gt;Excel Add Rows API&lt;/a&gt; answers this with a single HTTP call that takes a workbook and a JSON payload and hands back the updated workbook. No Excel installation, no COM interop, no spreadsheet library to patch every time Microsoft changes something. It's worth walking through exactly how it works, because the two insertion modes it supports, table mode and coordinate mode, are where most hand-rolled "just append a row" scripts eventually get it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Add Rows Endpoint Actually Does
&lt;/h2&gt;

&lt;p&gt;The REST endpoint is &lt;code&gt;POST office/ApiV2Excel/ExcelAddRows&lt;/code&gt;. Headers: &lt;code&gt;Content-Type: application/json&lt;/code&gt; and &lt;code&gt;Authorization: Basic YOUR_BASE64_ENCODED_API_KEY&lt;/code&gt; (get a key at the &lt;a href="https://dev.pdf4me.com/dashboard/#/api-keys/" rel="noopener noreferrer"&gt;PDF4me dashboard&lt;/a&gt;). You send a &lt;code&gt;document&lt;/code&gt; object holding the workbook's filename, a &lt;code&gt;docContent&lt;/code&gt; field with the workbook's Base64-encoded bytes, and an &lt;code&gt;addRowsToExcelAction&lt;/code&gt; object that carries the actual instructions. The one field every call needs inside that action object is &lt;code&gt;jsonInput&lt;/code&gt;: a JSON array of objects (or a single object) given as a string, where each object becomes one new row.&lt;/p&gt;

&lt;p&gt;That last detail trips people up the first time. &lt;code&gt;jsonInput&lt;/code&gt; isn't a nested JSON array in your request body; it's a string containing JSON, which means the quotes inside it have to be escaped.&lt;/p&gt;

&lt;p&gt;Sample request:&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;"document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data.xlsx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UEsDBBQABgAIAAAA..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"addRowsToExcelAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsonInput"&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="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Age&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:30}]"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns the updated Excel document as a Base64-encoded string in JSON, alongside &lt;code&gt;fileName&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, and &lt;code&gt;errorMessage&lt;/code&gt; fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Mode vs. Coordinate Mode
&lt;/h2&gt;

&lt;p&gt;This is the part a DIY script usually gets wrong, because it's invisible until it isn't. The API supports two different ways of deciding where new rows land, and mixing their parameters in the same call returns an error rather than a guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table mode&lt;/strong&gt; is for worksheets where the data already lives inside a real Excel Table object. Set &lt;code&gt;tableName&lt;/code&gt; to that table's name, and the API matches each JSON object's property names to the table's column headers, then inserts the row at the position given by &lt;code&gt;excelRowNumber&lt;/code&gt; (1-based). Matching happens by header name, not column order, so the order of keys in your JSON doesn't need to match the order of columns in the sheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordinate mode&lt;/strong&gt; is for a plain range of cells with no formal table object. Leave &lt;code&gt;tableName&lt;/code&gt; empty, and set &lt;code&gt;insertFromRow&lt;/code&gt; and &lt;code&gt;insertFromColumn&lt;/code&gt; (both 1-based) to the starting cell. Values are written in the order the JSON properties appear, with no header matching at all, which also means a coordinate-mode write can silently overwrite existing cells if the start position is wrong. There's no undo button on an API call, so get the starting cell right first.&lt;/p&gt;

&lt;p&gt;A few more fields round out the behavior: &lt;code&gt;convertNumericAndDate&lt;/code&gt; (default &lt;code&gt;true&lt;/code&gt;) turns a string like &lt;code&gt;"2026-10-08"&lt;/code&gt; into a real Excel date using &lt;code&gt;dateFormat&lt;/code&gt; (&lt;code&gt;yyyy-MM-dd&lt;/code&gt; default) and &lt;code&gt;numericFormat&lt;/code&gt; (&lt;code&gt;N2&lt;/code&gt; default), with &lt;code&gt;cultureName&lt;/code&gt; (&lt;code&gt;en-US&lt;/code&gt; default) controlling how ambiguous dates and decimals get parsed. &lt;code&gt;ignoreNullValues&lt;/code&gt; skips writing a cell at all when a property's value is null. &lt;code&gt;ignoreAttributeTitles&lt;/code&gt; makes table-mode header matching case-insensitive, which is the difference between a silent miss and a working match when your source sends &lt;code&gt;CustomerName&lt;/code&gt; and your table header is &lt;code&gt;customername&lt;/code&gt;. &lt;code&gt;worksheetName&lt;/code&gt; defaults to &lt;code&gt;Sheet1&lt;/code&gt; if omitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Beats Writing It Yourself
&lt;/h2&gt;

&lt;p&gt;A hand-rolled version in Python or C# isn't hard to start. It's hard to finish. You end up handling the table-vs-coordinate branching yourself, writing your own type-conversion logic for dates and numbers across locales, and testing against Excel quirks (shared formulas, conditional formatting that doesn't extend, a table that refuses to grow past its defined bounds) that only show up once real users send real data. A REST call with a JSON body and a Base64-encoded file is something almost any environment, including a serverless function with no business installing a spreadsheet library, can make without new dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using It Without Writing Code: Power Automate, Zapier, n8n, and Make
&lt;/h2&gt;

&lt;p&gt;The same action is available as a native step in the no-code automation platforms PDF4me integrates with.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/add-rows/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, the &lt;strong&gt;Excel - Add Rows&lt;/strong&gt; action takes the same fields as the REST call and returns the updated workbook plus &lt;code&gt;Success&lt;/code&gt;, &lt;code&gt;ErrorMessage&lt;/code&gt;, and &lt;code&gt;Errors&lt;/code&gt; a flow can branch on. Documented scenarios: a scheduled SQL query feeding a "DailySales" table every morning, a Microsoft Forms submission landing in the next open row of a response log saved back to SharePoint, and a weekly CRM export populated into an "Opportunities" table with case-insensitive header matching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/zapier/excel/add-rows/" rel="noopener noreferrer"&gt;Zapier's&lt;/a&gt; &lt;strong&gt;Add Rows to Excel&lt;/strong&gt; action follows the same shape, with a JSON array from an earlier Zap step mapped into the action's JSON Input field. Its own documented example, a daily sales export written into a templated "DailySales" table on Google Drive with date/currency formatting applied before upload and email, claims it saves roughly two hours of manual entry a day. Worth treating as Zapier's own stated estimate, not an independently verified number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/add-rows/" rel="noopener noreferrer"&gt;n8n's&lt;/a&gt; &lt;strong&gt;PDF4me Excel: Add Rows&lt;/strong&gt; node surfaces a detail the others don't: its output includes &lt;code&gt;rowsAdded&lt;/code&gt;, a count you can compare against your input array's length with an IF node to verify every row actually landed, rather than assuming success because the call didn't error. The same action is also available through &lt;a href="https://www.make.com/en/integrations/pdf4me-excel" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, PDF4me's fourth supported no-code platform, via its own app listing on Make's marketplace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits and Where It Doesn't
&lt;/h2&gt;

&lt;p&gt;This API appends rows to an existing workbook; it doesn't build one from scratch. For generating a brand-new Excel file from JSON data, see &lt;a href="https://docs.pdf4me.com/pdf4me-api/convert/convert-json-to-excel/" rel="noopener noreferrer"&gt;Convert JSON to Excel&lt;/a&gt;. For removing rows afterward, there's a matching &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/delete-rows/" rel="noopener noreferrer"&gt;Delete Rows&lt;/a&gt; endpoint built the same way.&lt;/p&gt;

&lt;p&gt;The honest limit: this is a synchronous, stateless call. It doesn't watch a folder, doesn't know your data source, and doesn't decide when to run; something else (a cron job, a webhook, a Power Automate trigger) still has to supply the workbook, the JSON, and the timing. What it removes is the part that used to require either a human with Excel open or a maintained library of spreadsheet-manipulation code.&lt;/p&gt;

&lt;p&gt;Sample code and SDKs: &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;pdf4me-api-samples on GitHub&lt;/a&gt;&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>productivity</category>
      <category>nocode</category>
    </item>
    <item>
      <title>How to Merge Multiple Excel Workbooks Into One File via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 08 Oct 2026 04:47:02 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-merge-multiple-excel-workbooks-into-one-file-via-api-59k0</link>
      <guid>https://dev.to/pdf4me/how-to-merge-multiple-excel-workbooks-into-one-file-via-api-59k0</guid>
      <description>&lt;h1&gt;
  
  
  How to Merge Multiple Excel Workbooks Into One File via API
&lt;/h1&gt;

&lt;p&gt;Every month-end close has the same ritual. Five regional managers each send their own spreadsheet. Someone, usually the least senior person on the finance team, opens all five, copies each tab into one master workbook, renames the sheets so they don't collide, and prays nobody fat-fingers a cell reference along the way. It's not hard work. It's just work that a computer should be doing, and isn't, because "just write a script to merge Excel files" turns out to hide more edge cases than it sounds like it does.&lt;/p&gt;

&lt;p&gt;What happens when two source files both have a sheet named "Summary"? What happens when one file has three worksheets and another has twelve, and you only want sheet two from each? These aren't hypothetical. They're the reason a one-line "combine these files" request usually turns into a half-day of writing and testing &lt;code&gt;openpyxl&lt;/code&gt; or EPPlus code, then maintaining it every time a source format changes.&lt;/p&gt;

&lt;p&gt;PDF4me's Excel Merge Files API handles this as a single HTTP call. Here's exactly how it works, including the request/response shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The REST Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST office/ApiV2Excel/ExcelMergeFiles
Content-Type: application/json
Authorization: Basic YOUR_BASE64_ENCODED_API_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request body, exactly as documented on the &lt;a href="https://docs.pdf4me.com/pdf4me-api/excel/merge-files/" rel="noopener noreferrer"&gt;Merge Files Excel API page&lt;/a&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;"mergeFilesToExcelAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file1.xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UEsDBBQABgAIAAAA..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file2.xlsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UEsDBBQABgAIAAAA..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputFileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merged.xlsx"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;A few things worth knowing before you copy this into your own code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fileContent&lt;/code&gt; is the Base64-encoded bytes of each source workbook, not a file path or URL.&lt;/li&gt;
&lt;li&gt;The API merges documents in array order, so East/West/Central/South ordering in a regional report comes from how you build this array, not from anything alphabetical.&lt;/li&gt;
&lt;li&gt;The docs page notes the casing is flexible: &lt;code&gt;documents&lt;/code&gt;/&lt;code&gt;Documents&lt;/code&gt;, &lt;code&gt;filename&lt;/code&gt;/&lt;code&gt;Filename&lt;/code&gt;, &lt;code&gt;fileContent&lt;/code&gt;/&lt;code&gt;FileContent&lt;/code&gt; all work.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputFileName&lt;/code&gt; is optional (defaults to &lt;code&gt;merged.xlsx&lt;/code&gt;), and there's also an optional &lt;code&gt;cultureName&lt;/code&gt; field (e.g. &lt;code&gt;en-US&lt;/code&gt;) for locale-sensitive processing, not shown above since the docs example omits it too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response comes back as JSON with &lt;code&gt;document&lt;/code&gt; (the merged workbook, Base64-encoded), &lt;code&gt;fileName&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, and &lt;code&gt;errorMessage&lt;/code&gt;. The docs page doesn't publish a full sample response body, so don't assume field nesting beyond those four names until you've seen a live response from your own account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding and Saving the Result (Python)
&lt;/h2&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="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;YOUR_BASE64_ENCODED_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/office/ApiV2Excel/ExcelMergeFiles&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;encode_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mergeFilesToExcelAction&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;documents&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;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;east.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;fileContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encode_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;east.xlsx&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;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;west.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;fileContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encode_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;west.xlsx&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;outputFileName&lt;/span&gt;&lt;span class="sh"&gt;"&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.xlsx&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;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="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;if&lt;/span&gt; &lt;span class="n"&gt;result&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;success&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;merged.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;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;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;document&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;Saved &lt;/span&gt;&lt;span class="si"&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;fileName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="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;Merge failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&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;errorMessage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A note on verification:&lt;/strong&gt; this sample uses only the request/response field names confirmed live on the docs page above. I wasn't able to cross-check against the official &lt;code&gt;pdf4me-api-samples&lt;/code&gt; GitHub repo this session (it blocked automated fetches), so if you hit a field name mismatch in practice, the live API response from your own account is the authority, not this snippet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Gets More Capable: The No-Code Integrations
&lt;/h2&gt;

&lt;p&gt;If you're not writing a custom integration, the no-code platform versions of this action expose more control than the bare REST contract above, specifically around worksheet selection and output format.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/power-automate/excel/merge-files/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;'s &lt;strong&gt;PDF4me Merge Excel Files&lt;/strong&gt; action takes a list of documents where each entry can include an optional list of worksheets to merge. Leave it empty and every sheet comes along; populate it and you pull only the sheets you want, useful when someone's quarterly report has a "scratch calculations" tab you don't want shipped to the board. It also lets you pick the output format: XLSX by default, or XLS, PDF, or CSV (CSV exports only the first worksheet).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/excel/merge-files/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;'s &lt;strong&gt;PDF4me Excel to Merge Files&lt;/strong&gt; module mirrors this with a &lt;code&gt;Files&lt;/code&gt; array where each item carries an optional &lt;code&gt;Sort Position&lt;/code&gt; (lower numbers merge first) and an optional &lt;code&gt;Worksheets To Merge&lt;/code&gt; list, plus the same four output formats. &lt;a href="https://docs.pdf4me.com/integration/zapier/excel/merge-files/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;'s &lt;strong&gt;PDF4me Merge Files&lt;/strong&gt; action automatically renames duplicate worksheet names across source files, so two files that both have a "Data" tab don't silently overwrite each other. &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-excel/merge-files/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;'s &lt;strong&gt;Merge multiple Excel files&lt;/strong&gt; node takes the same documents-plus-worksheets-plus-format shape, with the result landing as a native n8n binary object (correct Excel MIME type) so it flows straight into the next node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth flagging:&lt;/strong&gt; the REST API's own documentation doesn't describe an explicit output-format parameter the way all four integration platforms do. If you're calling REST directly, verify against your account's live response whether format selection applies there too, rather than assuming XLSX-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort Order and Worksheet Filtering, in Practice
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent across every platform: build a list of source files, give each an optional sort position (or rely on array order where that's all that's supported), and optionally name which worksheets you want. That's the whole decision tree, and it maps directly onto the actual common case: consolidating monthly reports where some files carry extra tabs you don't want, in an order that matters to whoever reads the output.&lt;/p&gt;

&lt;p&gt;The duplicate-worksheet-name handling matters more than it sounds. Two regional reports both naming their summary tab "Summary" is extremely normal. A hand-rolled merge script that doesn't think about this will silently let the second "Summary" overwrite the first, and nobody notices until finance asks why the East region's numbers disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Fits in a Bigger Pipeline
&lt;/h2&gt;

&lt;p&gt;Merging rarely happens in isolation. It's usually one step: pull five regional exports from a shared drive, merge them, maybe convert the result to PDF for archival, then email it out. Because this is one HTTP call (or one no-code module) rather than a library you install and maintain, it slots into a Power Automate flow triggered by a new SharePoint file, a Make scenario watching a Drive folder, a Zapier zap off a form submission, or an n8n workflow chained with other document steps. PDF4me's broader &lt;a href="https://pdf4me.com/office-api" rel="noopener noreferrer"&gt;Word and Excel API&lt;/a&gt; covers the surrounding operations too: splitting workbooks back apart, comparing versions, watermarking, password protection, and PDF-to-Office conversions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Limits
&lt;/h2&gt;

&lt;p&gt;This endpoint merges workbooks and lets you choose worksheets and order. It does not reconcile conflicting cell formulas across files, does not deduplicate rows that mean the same thing but are phrased differently, and won't catch a schema mismatch where one region's spreadsheet has an extra column the others don't. Those are data-modeling problems upstream of the merge, not inside it. What it reliably removes is the tedious part: opening N files, copying N sets of tabs, hoping you didn't drop a sheet or get the order wrong.&lt;/p&gt;

&lt;p&gt;If you're maintaining an in-house script for this today, a quick gut check: how long would it take to add "exclude the scratch tab" as a new requirement in your existing code, versus adding one &lt;code&gt;worksheetsToMerge&lt;/code&gt; field to a request you're already sending?&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Extract Line Items, Totals, and Customer Info from Purchase and Sales Orders via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:46:14 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-extract-line-items-totals-and-customer-info-from-purchase-and-sales-orders-via-api-3lm8</link>
      <guid>https://dev.to/pdf4me/how-to-extract-line-items-totals-and-customer-info-from-purchase-and-sales-orders-via-api-3lm8</guid>
      <description>&lt;p&gt;Every procurement, fulfillment, or ERP-adjacent team ends up building the same throwaway tool eventually: something that opens a purchase order PDF, finds the line items, finds the total, and matches it against whatever system of record actually runs the business. Then a new supplier sends a PO in a different layout, the regex breaks, and someone spends an afternoon patching a parser nobody wanted to own in the first place.&lt;/p&gt;

&lt;p&gt;Purchase orders and sales orders are two sides of the same transaction (one side issues it, the other side fulfills it) but they get treated as a document-handling problem twice, once per direction, because most parsing tools make you describe the layout before they'll read it. That's the part worth fixing, not the OCR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one parser has to cover both documents
&lt;/h2&gt;

&lt;p&gt;A purchase order and a sales order carry almost the same information: an order number, a date, who it's for, what's being ordered, how many, at what price, and what the total comes to. The difference is which side of the transaction wrote it. A buyer's procurement system generates POs; a seller's order-entry system generates SOs. Downstream, a reconciliation or fulfillment system usually needs to read both, because one company's outgoing PO is often another company's incoming SO in the same workflow.&lt;/p&gt;

&lt;p&gt;That shared shape is exactly why a schema-driven parser works here in a way that template matching doesn't. A capture-area tool anchors to pixel coordinates, so a vendor that moves their logo six millimeters to the left breaks the template. A regex-based tool anchors to text patterns, so "Total:" vs "Total Due:" vs "Amount Due" each needs its own rule. Neither approach generalizes across a supplier list you don't control the formatting of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Order Parser actually returns
&lt;/h2&gt;

&lt;p&gt;PDF4me's AI Order Parser skips capture areas and regex entirely: you send it a PDF or image, it returns a fixed JSON shape, and per &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-order/" rel="noopener noreferrer"&gt;PDF4me's own Make.com integration documentation&lt;/a&gt; that shape is identical whether the source document is a purchase order or a sales order ("works without template setup," "the two document types return the same schema").&lt;/p&gt;

&lt;p&gt;The documented output fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;orderNumber&lt;/code&gt; (string): order or reference number, whatever format the document uses (&lt;code&gt;PO-2024-001&lt;/code&gt;, &lt;code&gt;SO-78423&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;orderDate&lt;/code&gt; (string): normalized to ISO 8601 (&lt;code&gt;YYYY-MM-DD&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customerName&lt;/code&gt; (string): the customer or vendor name on the document&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lineItems&lt;/code&gt; (array): each row carries &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;quantity&lt;/code&gt;, &lt;code&gt;unitPrice&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subTotal&lt;/code&gt; (number): amount before taxes, shipping, discounts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total&lt;/code&gt; (number): final amount including all charges&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;currency&lt;/code&gt; (string): ISO 4217 code (&lt;code&gt;USD&lt;/code&gt;, &lt;code&gt;EUR&lt;/code&gt;, &lt;code&gt;INR&lt;/code&gt;, ...)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shippingAddress&lt;/code&gt; (string): shipping or delivery address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jobId&lt;/code&gt; (string): PDF4me's own job ID, for audit/support&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;success&lt;/code&gt; (boolean) / &lt;code&gt;message&lt;/code&gt; (string): completion status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A realistic response:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-e5f6-4789-9abc-def012345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PO-2024-00187"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Meridian Fabrication Ltd."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lineItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M8 Stainless Hex Bolt, 40mm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;210.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M8 Stainless Washer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.06&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;30.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subTotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;240.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;259.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shippingAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4410 Industrial Pkwy, Unit 12, Dayton, OH 45417"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Extraction completed."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's enough to populate an ERP line, run a reconciliation threshold check, or trigger a fulfillment workflow without a human opening the PDF first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A documentation gap worth being upfront about:&lt;/strong&gt; there's no published REST-only walkthrough for this specific action on &lt;code&gt;docs.pdf4me.com&lt;/code&gt; (confirmed 404, not assumed), and the official &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;pdf4me-api-samples repo on GitHub&lt;/a&gt; doesn't have an Order Parser folder either, unlike several of the other AI parsers in this cluster. Make.com is currently the one platform with a dedicated, field-by-field written guide for this action. If you're calling this over the raw REST API, the schema above (verified against Make's docs) is what to expect back, but test against your own documents before you build a strict validator around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A second gap, this one a discrepancy rather than a missing page:&lt;/strong&gt; PDF4me's Power Automate connector (listed on &lt;a href="https://learn.microsoft.com/ja-jp/connectors/pdf4meai" rel="noopener noreferrer"&gt;Microsoft's own connector reference&lt;/a&gt; as &lt;code&gt;AI - Order Parser&lt;/code&gt; / operation ID &lt;code&gt;ProcessOrderAi&lt;/code&gt;) documents a noticeably different output shape for what is nominally the same action, built around &lt;code&gt;invoiceToName&lt;/code&gt;/&lt;code&gt;deliverToName&lt;/code&gt; pairs, a &lt;code&gt;products&lt;/code&gt; array, and a few fields that read like leftovers from one specific customer's template rather than a general-purpose schema. That's either a different model version, a stale page on Microsoft's side, or a connector-specific response wrapper, and there's no way to tell which from the outside. If you're building on Power Automate, run a live test order through the action and check the actual field names it returns before wiring a Flow around either schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in a no-code or custom stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make&lt;/strong&gt;: the one platform with a dedicated, written walkthrough for the AI-Process Order module, including the field table above. Map &lt;strong&gt;Order Name&lt;/strong&gt; and the document binary, run an Iterator over &lt;code&gt;lineItems&lt;/code&gt; if you need one ERP row per line item, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power Automate&lt;/strong&gt;: the same capability exists as a native connector action (&lt;code&gt;ProcessOrderAi&lt;/code&gt;), available across Power Automate, Power Apps, and Copilot Studio (Premium) per Microsoft's own connector listing, with a 100-calls-per-60-seconds throttle per connection. Verify the live output fields yourself, per the discrepancy noted above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt;: PDF4me's broader AI parser family is reachable directly for teams building their own backend instead of a no-code flow, following the same API key and connection pattern as PDF4me's other endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the fixed schema isn't enough&lt;/strong&gt;: orders carrying fields the standard parser doesn't name (a custom SKU format, a project code, a buyer-specific approval field) are a better fit for PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-classify/" rel="noopener noreferrer"&gt;Universal Document Parser&lt;/a&gt;, which lets you specify the exact fields to extract, or the &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-parse/" rel="noopener noreferrer"&gt;Document Parser / custom analyzer path&lt;/a&gt;, which lets you define a reusable extraction profile in the PDF4me dashboard. Reach for either one before writing a post-processing script to bolt extra fields onto the Order Parser's output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual use case this solves
&lt;/h2&gt;

&lt;p&gt;Picture a small distributor that takes purchase orders by email, as PDFs, from forty different retail accounts, each using whatever order-entry system that retailer happens to run. Nobody on the distributor's side wants to build forty templates. Dropping every incoming PO through the Order Parser, regardless of sender, normalizes them all into the same &lt;code&gt;orderNumber&lt;/code&gt; / &lt;code&gt;lineItems&lt;/code&gt; / &lt;code&gt;total&lt;/code&gt; shape before anything touches the warehouse management system. The matching logic downstream (does this line item exist in inventory, does the total reconcile, does this need a human to look at it) only has to be written once, against one schema, instead of once per retailer.&lt;/p&gt;

&lt;p&gt;The same logic runs in reverse for a seller reconciling their own outgoing sales order confirmations against what a buyer's procurement system actually received, which is the other half of why one shared schema across both document types is worth more than it sounds.&lt;/p&gt;

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

&lt;p&gt;This parser reads documents, it doesn't validate business logic. It won't tell you whether a &lt;code&gt;total&lt;/code&gt; is actually correct given the &lt;code&gt;lineItems&lt;/code&gt; listed (that's a reconciliation check you still write), and it won't catch a line item that's missing entirely if the source document itself never listed it. Treat the extraction as trustworthy input to your own validation step, not a replacement for one.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Parse W-2, 1099, and 1040 Tax Forms Automatically with One API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:23:58 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-parse-w-2-1099-and-1040-tax-forms-automatically-with-one-api-2jad</link>
      <guid>https://dev.to/pdf4me/how-to-parse-w-2-1099-and-1040-tax-forms-automatically-with-one-api-2jad</guid>
      <description>&lt;p&gt;Tax season turns into a document-matching exercise for a lot of software teams that have nothing to do with accounting. A lending platform verifying income needs the wages box off a W-2. A background-check or underwriting tool needs the adjusted gross income line off a 1040. An HR onboarding flow needs the withholding figures off a W-4. None of these teams want to become tax-form experts. They just want the handful of numbers that matter, pulled out of a PDF someone uploaded, without building a different parser for every form a tax season throws at them.&lt;/p&gt;

&lt;p&gt;That is a harder problem than it sounds, and it is worth being specific about why before looking at the API that solves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why One Tax Form Parser Has to Behave Like Thirteen
&lt;/h2&gt;

&lt;p&gt;A W-2 and a 1040 do not share a layout, a vocabulary, or even a purpose. One is an employer's wage statement. The other is a full individual income tax return with dozens of line items. A 1098 reports mortgage interest. A 1095-A reports health insurance coverage months. Build a parser tuned to a W-2's box numbers and it has nothing useful to say about a 1040. Build a generic OCR pipeline instead and you get back a wall of unlabeled text, with a human, or another piece of custom code, left to figure out which number is adjusted gross income and which one is a state tax withholding on a completely different form.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;AI Tax Document Parser&lt;/a&gt; is built around that specific problem: one API surface that identifies which of thirteen named form types it is looking at, then returns the field set that actually applies to that form. Feed it a W-2 and it returns W-2 fields. Feed it a 1040-SR and it returns 1040 fields, not an empty W-2 template with nothing filled in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Response Envelope, Field for Field
&lt;/h2&gt;

&lt;p&gt;Every response, regardless of which form came in, carries the same core envelope:&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;"formType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"W2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"taxYear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"employerName"&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;"employerEin"&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;"wagesTips"&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;"socialSecurityWages"&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;"medicareWages"&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;"stateWages"&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;"stateIncomeTax"&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="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallbackUsed"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawOcrText"&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;"jobId"&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That envelope shape is confirmed identical across the &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-tax-document-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-tax-document/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-tax-document-parser/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt; documentation pages for this parser. None of the three publishes a full sample response with real values, so the field names above are drawn directly from each page's documented output schema, not copied from an example payload that does not exist yet on any of them.&lt;/p&gt;

&lt;p&gt;What changes between calls is what lands inside &lt;code&gt;fields&lt;/code&gt;. The parser recognizes thirteen named form types across six categories: wage statements (W2), information returns (1099 and 1099-SSA), individual returns (1040, 1040-SR, 1040-NR), interest and education statements (1098, 1098-E, 1098-T), health coverage forms (1095A, 1095C), and withholding declarations (W-4), plus a catch-all UnifiedTaxUS classification. A 1040 response fills &lt;code&gt;fields&lt;/code&gt; with filing status, taxpayer name, taxpayer SSN, address, wages, total income, adjusted gross income, taxable income, total tax, federal income tax withheld, and refund amount instead of the W-2 fields shown above. A 1098 response fills it with mortgage interest, student loan interest received, and qualified tuition and related expenses. Across all thirteen form types combined, the documented field vocabulary runs past sixty distinct values, but no single response returns all sixty at once, only the subset that belongs to whatever form type &lt;code&gt;formType&lt;/code&gt; just identified.&lt;/p&gt;

&lt;p&gt;Treat &lt;code&gt;formType&lt;/code&gt; as more than metadata. It is the signal that tells your downstream code which keys to even expect in &lt;code&gt;fields&lt;/code&gt; before it tries to read them. A pipeline that reads &lt;code&gt;fields.wages&lt;/code&gt; off every response without checking &lt;code&gt;formType&lt;/code&gt; first will quietly break the moment a 1098 shows up where a W-2 was expected, since &lt;code&gt;fields.wages&lt;/code&gt; simply will not exist on that response.&lt;/p&gt;

&lt;p&gt;One documentation discrepancy worth flagging directly: PDF4me's own n8n page groups the same thirteen form codes into what it calls twelve document categories (1099 and 1099-SSA counted together under one category there), while the Power Automate page counts thirteen form types directly. The supported forms are identical either way. This is a grouping difference between two docs pages, not a coverage gap, so do not be surprised seeing both numbers in the wild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Error Handling Around warnings and fallbackUsed, Not Just the Dollar Fields
&lt;/h2&gt;

&lt;p&gt;Three fields in that envelope matter more than the numbers they sit next to. &lt;code&gt;warnings&lt;/code&gt; flags extractions the parser itself is not confident about. &lt;code&gt;fallbackUsed&lt;/code&gt; tells you whether it had to fall back to a secondary extraction path to get an answer at all. &lt;code&gt;rawOcrText&lt;/code&gt; hands a human reviewer the raw text to check against when something looks off. A tax document feeding a loan decision or a background check is exactly the kind of input where a quietly wrong number is worse than a clearly flagged uncertain one. A pipeline that reads the dollar fields and discards &lt;code&gt;warnings&lt;/code&gt; is throwing away the one signal built specifically to catch a bad extraction before it reaches a human decision downstream.&lt;/p&gt;

&lt;p&gt;There is also an optional &lt;code&gt;customFieldKeys&lt;/code&gt; parameter for pulling additional fields beyond the standard set for a given form type, documented on the Power Automate page as an Advanced parameter alongside &lt;code&gt;TaxModel&lt;/code&gt;, for workflows that need one more data point than the built-in schema covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Dedicated REST Page, Same Pattern as This Parser's Siblings
&lt;/h2&gt;

&lt;p&gt;Anyone looking for a plain REST endpoint outside the four no-code platforms will not find one. There is no dedicated &lt;code&gt;pdf4me-api&lt;/code&gt; page for this specific parser; &lt;code&gt;https://docs.pdf4me.com/pdf4me-api/pdf4me-ai/ai-tax-document-parser/&lt;/code&gt; returns a 404, confirmed directly while researching this piece, the same finding this cluster has run into for the AI Mortgage Document Parser and the AI Pay Stub/Payslip Parser before it.&lt;/p&gt;

&lt;p&gt;What the REST layer gives you instead is the generic &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-parse/" rel="noopener noreferrer"&gt;Analyzer mechanism&lt;/a&gt; that every named parser in PDF4me's AI lineup sits on top of. You define a &lt;code&gt;fieldName&lt;/code&gt;, &lt;code&gt;fieldType&lt;/code&gt;, and &lt;code&gt;fieldDescription&lt;/code&gt; for each value you want back, save it under a stable &lt;code&gt;AnalyzerId&lt;/code&gt; in the dashboard, and that identifier is what every subsequent API call references. The documented request shape, confirmed live against that page, is:&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;"w2_form.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;"BASE64_ENCODED_PDF_CONTENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AnalyzerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tax_document_parser"&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;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;sent as a &lt;code&gt;POST&lt;/code&gt; to the base API confirmed on PDF4me's &lt;a href="https://docs.pdf4me.com/general-guidelines/connect-to-pdf4meapi/" rel="noopener noreferrer"&gt;connect-to-pdf4meapi&lt;/a&gt; page: &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, at the &lt;code&gt;/api/v2/&lt;/code&gt; path. The named Tax Document Parser covered above is the faster path when the documents really are standard W-2s, 1099s, and 1040s, since the thirteen-form schema is already built for you. The Analyzer route is what you reach for when a tax-adjacent document falls outside those thirteen types, because &lt;code&gt;fieldDescription&lt;/code&gt; lets you point the model at exactly the value you want instead of picking from a fixed list.&lt;/p&gt;

&lt;p&gt;One more honest gap worth naming: the official &lt;a href="https://github.com/pdf4me/pdf4me-api-samples" rel="noopener noreferrer"&gt;pdf4me-api-samples&lt;/a&gt; repository, which has working code for most of PDF4me's REST features in C#, Java, Python, JavaScript, and Apex, currently has no folder for tax document parsing specifically, since this AI parser family is newer than that repo's existing coverage. The Analyzer request shape above is confirmed from the docs page itself, not from a tested sample script, so verify the exact response shape against your own test call before shipping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Platforms Fully Documented, One Still a Stub
&lt;/h2&gt;

&lt;p&gt;In Power Automate, the action drops into a flow the way any other step does: a document lands by email or form upload, the action returns the structured fields, and the flow routes them wherever onboarding or underwriting logic lives next. In n8n, the node covers the self-hosted or more code-adjacent case, sitting inside a larger automation that might also write to a database. In Zapier, the same extraction becomes a step that can follow a Gmail attachment trigger or a Drive upload.&lt;/p&gt;

&lt;p&gt;Make is worth a direct note rather than a smoothed-over one. Its &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-tax-document/" rel="noopener noreferrer"&gt;own integration page&lt;/a&gt; is live, but it is currently a stub, labeled on the page itself as adapted from the n8n equivalent with full documentation still pending. The module exists and the supported form list matches the other three platforms, but anyone building against it today should verify the exact field output in a real test call rather than relying on the docs page for a complete field table the way they could for Power Automate, n8n, or Zapier.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Case: Income Verification Across Mismatched Form Types
&lt;/h2&gt;

&lt;p&gt;Picture a lending team that collects income documentation from applicants who do not all submit the same kind of paperwork. A salaried applicant uploads a W-2. A self-employed applicant uploads a 1099. Someone refinancing uploads a 1098 showing mortgage interest paid. Today, a human opens each one, works out which document type it even is, and manually keys the relevant figures into the loan file.&lt;/p&gt;

&lt;p&gt;Wiring this parser into that intake point changes the shape of the work rather than just speeding up the typing. Every upload gets the same call. &lt;code&gt;formType&lt;/code&gt; tells the system which document just arrived without a human needing to eyeball it first, and &lt;code&gt;fields&lt;/code&gt; already matches that form type, whether that means wages and withholding from a W-2 or nonemployee compensation from a 1099. &lt;code&gt;warnings&lt;/code&gt; routes the handful of genuinely uncertain extractions to a reviewer instead of routing every single document to one. The reviewer's job shifts from re-typing numbers and sorting document types to judging the small number of edge cases the parser itself flagged. The same pattern applies past lending: a background-check platform reconciling a 1040's adjusted gross income against a stated salary, or an HR team processing a new hire's W-4 withholding elections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;If the documents in question are genuinely one of the thirteen supported form types, the named parser is the faster path. No schema to design, just a document in and the right field set out, with &lt;code&gt;formType&lt;/code&gt; telling you which set you got. Test it against real forms from an actual tax season rather than a single clean sample, since form layouts shift year to year. Build your error handling around &lt;code&gt;warnings&lt;/code&gt; and &lt;code&gt;fallbackUsed&lt;/code&gt; from the start rather than retrofitting it after a bad extraction reaches a decision it should not have. If a document falls outside the thirteen supported types, or you are on Make and need more certainty than today's stub page can offer, the Analyzer-based route above is the fallback that does not require waiting on anyone else's documentation to catch up.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Automate Payroll Data Entry: Extracting Gross Pay, Deductions, and YTD Totals from Pay Stubs via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Wed, 07 Oct 2026 04:27:23 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-automate-payroll-data-entry-extracting-gross-pay-deductions-and-ytd-totals-from-pay-stubs-364j</link>
      <guid>https://dev.to/pdf4me/how-to-automate-payroll-data-entry-extracting-gross-pay-deductions-and-ytd-totals-from-pay-stubs-364j</guid>
      <description>&lt;h1&gt;
  
  
  How to Automate Payroll Data Entry: Extracting Gross Pay, Deductions, and YTD Totals from Pay Stubs via API
&lt;/h1&gt;

&lt;p&gt;Ask anyone who has processed payroll by hand what breaks first, and they will not say the math. They will say the typing. A pay stub looks like one document, but it is actually a dozen small facts packed onto one page: gross pay, net pay, four or five separate tax withholdings, overtime hours at a different rate than regular hours, a 401(k) deduction, a year-to-date column that has to match last month's year-to-date column plus this period's numbers. Copy one of those wrong into a loan application, a background-check report, or an HR system, and the error does not announce itself. It just sits there until someone's income verification comes back inconsistent with their bank statement.&lt;/p&gt;

&lt;p&gt;That is the specific failure a document parser is built to prevent, and it is worth being precise about what the API actually returns before wiring anything up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Pay Stub Is Harder to Automate Than It Looks
&lt;/h2&gt;

&lt;p&gt;Every payroll system lays its stub out differently. ADP, Gusto, Paychex, and a thousand smaller in-house systems each put gross pay in a different spot on the page, label overtime differently, and format a pay period as a date range, a single end date, or a cycle number. A regex rule tuned to one format breaks the moment a new employer's stub shows up. A generic OCR pass reads the characters correctly and then hands back forty lines of unlabeled text, leaving a human, or a second piece of custom code, to figure out which number is the employer's EIN and which one is a garnishment.&lt;/p&gt;

&lt;p&gt;PDF4me's &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;AI Pay Stub/Payslip Parser&lt;/a&gt; exists to skip that step. It reads the document, whatever its layout, and returns the same structured fields every time: &lt;code&gt;grossPay&lt;/code&gt; is always &lt;code&gt;grossPay&lt;/code&gt;, whether the source stub came from a national payroll provider or a spreadsheet someone printed to PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Exact Fields That Come Back
&lt;/h2&gt;

&lt;p&gt;Here is the field list as documented on the &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-pay-stub-payslip-parser/" rel="noopener noreferrer"&gt;Power Automate integration page&lt;/a&gt;, which gives the most complete output table of the four platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grossPay              netPay                payPeriod
payDateStr            federalIncomeTax      stateIncomeTax
socialSecurityTax     medicareTax           localCityTax
employeeName          employeeIdSsn         employeeAddress
jobTitle              companyName           employerAddress
employerEin           regularHours          overtimeHours
regularRate           overtimeRate          totalHours
healthInsurance       retirement401k        otherBenefits
garnishments          ytdGrossPay           ytdNetPay
ytdFederalTax         ytdStateTax           checkNumber
directDepositInfo     vacationSickTime      commissionBonus
warnings              fallbackUsed          rawOcrText
jobId                 jobIdExt              success
message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 41 distinct keys in that page's own output table (40 data fields plus a wrapping &lt;code&gt;fields&lt;/code&gt; object). Do not treat that as a universal total, though. The &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-pay-stub-payslip/" rel="noopener noreferrer"&gt;n8n node's&lt;/a&gt; own &lt;code&gt;payStubData&lt;/code&gt; object documents 40 named fields, the &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-pay-stub-payslip/" rel="noopener noreferrer"&gt;Make module's&lt;/a&gt; FAQ cites 19 in its primary output table with more in extended output, and &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-pay-stub-payslip-parser" rel="noopener noreferrer"&gt;Zapier's page&lt;/a&gt; gives no total at all. The field sets overlap heavily rather than genuinely differing, so write your integration against the fields you actually pull back in a real test call, not against whichever integration page's count you read first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;warnings&lt;/code&gt;, &lt;code&gt;fallbackUsed&lt;/code&gt;, and &lt;code&gt;rawOcrText&lt;/code&gt; are worth building around deliberately rather than ignoring: &lt;code&gt;warnings&lt;/code&gt; flags low-confidence extractions, &lt;code&gt;fallbackUsed&lt;/code&gt; tells you whether the parser fell back to a secondary extraction path, and &lt;code&gt;rawOcrText&lt;/code&gt; gives a human reviewer the raw text to check against when something looks off. A pipeline that reads &lt;code&gt;success&lt;/code&gt; and the dollar fields but throws away &lt;code&gt;warnings&lt;/code&gt; is discarding the one signal built specifically to catch a bad extraction before it reaches a loan file or an HR record.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Dedicated REST Page, and What That Means for a Custom Integration
&lt;/h2&gt;

&lt;p&gt;Worth noting for anyone looking for a plain REST endpoint outside the four no-code platforms: there is no dedicated &lt;code&gt;pdf4me-api&lt;/code&gt; page for this specific parser (confirmed as a 404 on the expected path while researching this piece, same finding as this cluster's earlier AI Mortgage Document Parser article). What the REST layer gives you instead, per the &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-parse/" rel="noopener noreferrer"&gt;general-guidelines AI Document Parser (Parse) page&lt;/a&gt;, is the generic Analyzer mechanism that every named parser in PDF4me's AI lineup sits on top of. You create an Analyzer in the dashboard (a user-defined identifier, e.g. &lt;code&gt;pay_stub_parser&lt;/code&gt;), specify a &lt;code&gt;fieldName&lt;/code&gt;, &lt;code&gt;fieldType&lt;/code&gt; (&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, or &lt;code&gt;table&lt;/code&gt;), and a &lt;code&gt;fieldDescription&lt;/code&gt; per value you want back, and that stable &lt;code&gt;AnalyzerId&lt;/code&gt; is what every subsequent API call references. The request body shape, as documented on that page, is:&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;"pay_stub.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;"BASE64_ENCODED_PDF_CONTENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AnalyzerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay_stub_parser"&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;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;against the base API documented at &lt;a href="https://docs.pdf4me.com" rel="noopener noreferrer"&gt;&lt;code&gt;general-guidelines/connect-to-pdf4meapi&lt;/code&gt;&lt;/a&gt;: &lt;code&gt;https://api.pdf4me.com&lt;/code&gt;, over &lt;code&gt;POST&lt;/code&gt;. The named, no-code-platform parser (the one this article is mostly about) is the faster path when the documents really are standard pay stubs and the fixed field list above already covers what you need. The Analyzer route is what you reach for instead when a field you need is not on that list, or the document is payroll-adjacent but not a standard pay stub, since &lt;code&gt;fieldDescription&lt;/code&gt; lets you point the model at exactly the value you want rather than picking from a preset schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Extraction, Four Front Doors
&lt;/h2&gt;

&lt;p&gt;The underlying extraction is the same regardless of how you trigger it, but the way you wire it into a workflow depends on where that workflow already lives.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-pay-stub-payslip-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, the connector slots into a flow the way any other action does: drop in a pay stub from email, SharePoint, or a form upload, get structured payroll fields back, and route them into whatever system handles onboarding or payroll review next. In &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-pay-stub-payslip/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, the module does the same job inside a scenario, a natural fit if the pay stub is already arriving through a Make-connected inbox, cloud drive, or form tool. &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-pay-stub-payslip/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; covers the self-hosted or more code-adjacent case, where the node sits inside a larger automation that might also touch a database or an internal API. And in &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-pay-stub-payslip-parser" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, the same extraction becomes a step that can follow a Gmail attachment trigger or a Google Drive upload, often the fastest way for a smaller HR or lending team to get this running without writing any code at all.&lt;/p&gt;

&lt;p&gt;Pick the surface that matches where the pay stubs already land, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Case: Income Verification Without Re-Typing Anything
&lt;/h2&gt;

&lt;p&gt;Picture a small lending team that verifies income manually today. An applicant uploads two or three recent pay stubs through a web form or emails them in. Someone opens each PDF, reads off gross pay and net pay, checks &lt;code&gt;ytdGrossPay&lt;/code&gt; against roughly three times the pay period figure to catch an obviously stale or doctored document, and keys everything into the loan file by hand. Multiply that by dozens of applications a week and the bottleneck is never the underwriting logic. It is the data entry in front of it.&lt;/p&gt;

&lt;p&gt;Wiring the parser into that same intake point changes the shape of the work rather than just speeding it up. A form submission or an inbox rule triggers the extraction the moment a pay stub arrives. &lt;code&gt;grossPay&lt;/code&gt; and &lt;code&gt;netPay&lt;/code&gt; populate the income fields directly, the YTD fields feed the same cross-check a human reviewer would have run by hand, and &lt;code&gt;warnings&lt;/code&gt; routes the handful of genuinely uncertain documents to a reviewer instead of all of them. The reviewer's job shifts from re-typing numbers to judging edge cases, which is a better use of a trained underwriter's time either way. The same pattern holds for an HR team onboarding a transferring employee, or a payroll audit diffing a sample of stubs against a company's own payroll register.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;If the documents in question are genuinely pay stubs or payslips, the named parser is the faster path. No schema to design, no field mapping to maintain, just a document in and the field list above back out. Test it against real stubs from whichever payroll providers your actual documents come from, since format variation between providers is exactly the problem this parser is built to absorb, and build your error handling around &lt;code&gt;warnings&lt;/code&gt; and &lt;code&gt;fallbackUsed&lt;/code&gt; rather than only the happy-path fields. Reach for the Analyzer-based route the moment the documents stop being standard pay stubs.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>ai</category>
      <category>fintech</category>
    </item>
    <item>
      <title>What Does an AI Mortgage Document Parser Actually Return? Loan Terms, Borrower Info, and Closing Details Explained</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:45:03 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-does-an-ai-mortgage-document-parser-actually-return-loan-terms-borrower-info-and-closing-4p6a</link>
      <guid>https://dev.to/pdf4me/what-does-an-ai-mortgage-document-parser-actually-return-loan-terms-borrower-info-and-closing-4p6a</guid>
      <description>&lt;h1&gt;
  
  
  What Does an AI Mortgage Document Parser Actually Return? Loan Terms, Borrower Info, and Closing Details Explained
&lt;/h1&gt;

&lt;p&gt;Hand a mortgage file to a generic OCR tool and you get a transcript: every character on every page, roughly in reading order, with no idea which number is the loan amount and which one is the appraised value. That distinction is the whole job in a loan-processing or compliance pipeline, where the next system in line needs &lt;code&gt;loanAmount&lt;/code&gt; in one field and &lt;code&gt;appraisedValue&lt;/code&gt; in another, not forty pages of text it has to re-read itself. A mortgage package makes this worse than almost any other document type, because it is not one document. It is a stack: a promissory note, a deed of trust, an appraisal, a closing disclosure, sometimes a title report, each with its own layout and its own vocabulary for the same underlying facts. So what does a purpose-built mortgage document parser actually hand back, and why does its field list look the way it does?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Transcript Is Not a Loan File
&lt;/h2&gt;

&lt;p&gt;Lenders, servicers, and the software vendors who build for them do not want text. They want a loan amount they can run a calculation against, a closing date they can schedule a disbursement around, and a borrower name they can match to an existing record, every time, regardless of which title company or loan origination system produced the page. A generic OCR engine reads a mortgage document correctly as characters and still leaves someone to figure out which string means what, document by document, lender by lender. That is the gap PDF4me's &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;AI Mortgage Document Parser&lt;/a&gt; is built to close. According to its own product description, it uses "machine learning to extract structured data from mortgage documents," which in practice means it understands mortgage paperwork well enough to return loan terms, borrower details, and closing information as named fields, not as a page of recovered text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Response Shape, Field by Field
&lt;/h2&gt;

&lt;p&gt;Send the parser a mortgage document as a PDF, PNG, JPG, or JPEG, and the response groups into several clear categories. I live-checked the field list directly against PDF4me's own Power Automate and n8n integration docs for this action rather than trusting a single summary table, and it is worth doing the same before you wire anything to it, because the two pages don't phrase the grouping identically even though the field names match.&lt;/p&gt;

&lt;p&gt;Loan information: &lt;code&gt;documentType&lt;/code&gt;, &lt;code&gt;lenderName&lt;/code&gt;, &lt;code&gt;loanAmount&lt;/code&gt;, &lt;code&gt;interestRate&lt;/code&gt;, &lt;code&gt;loanTerm&lt;/code&gt;, &lt;code&gt;loanType&lt;/code&gt;, &lt;code&gt;effectiveDate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Borrower details: &lt;code&gt;borrowerName&lt;/code&gt;, &lt;code&gt;coBorrowerName&lt;/code&gt;, &lt;code&gt;employerName&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;income&lt;/code&gt;, &lt;code&gt;employmentStatus&lt;/code&gt;, &lt;code&gt;employmentStartDate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Property and appraisal: &lt;code&gt;propertyAddress&lt;/code&gt;, &lt;code&gt;legalDescription&lt;/code&gt;, &lt;code&gt;propertyType&lt;/code&gt;, &lt;code&gt;conditionRating&lt;/code&gt;, &lt;code&gt;appraisedValue&lt;/code&gt;, &lt;code&gt;appraiserName&lt;/code&gt;, &lt;code&gt;appraiserLicenseNumber&lt;/code&gt;, plus a &lt;code&gt;comparableSales&lt;/code&gt; array holding each comparable's address, sale price, sale date, and adjustments.&lt;/p&gt;

&lt;p&gt;Closing and title: &lt;code&gt;closingCosts&lt;/code&gt;, &lt;code&gt;cashToClose&lt;/code&gt;, &lt;code&gt;settlementAgent&lt;/code&gt;, &lt;code&gt;closingDate&lt;/code&gt;, &lt;code&gt;disbursementDate&lt;/code&gt;, &lt;code&gt;currentOwner&lt;/code&gt;, &lt;code&gt;liensAndEncumbrances&lt;/code&gt;, &lt;code&gt;easements&lt;/code&gt;, &lt;code&gt;titleInsuranceAmount&lt;/code&gt;, &lt;code&gt;propertyTaxesStatus&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Payments and compliance: a &lt;code&gt;projectedPayments&lt;/code&gt; array (one entry per payment type, each carrying principal and interest, mortgage insurance, estimated escrow, and the estimated total monthly payment), plus &lt;code&gt;notarySignatureWitness&lt;/code&gt;, &lt;code&gt;controlNumber&lt;/code&gt;, &lt;code&gt;parish&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;witnessName&lt;/code&gt;, &lt;code&gt;witnessType&lt;/code&gt;, &lt;code&gt;preparerCompany&lt;/code&gt;, &lt;code&gt;homesteadActCompliance&lt;/code&gt;, &lt;code&gt;farmLandActCompliance&lt;/code&gt;, and a handful of lien-related flags: &lt;code&gt;securesLiabilities&lt;/code&gt;, &lt;code&gt;guaranteeMortgage&lt;/code&gt;, &lt;code&gt;collateralMortgage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Processing metadata on every response: &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;warnings&lt;/code&gt;, &lt;code&gt;fallbackUsed&lt;/code&gt;, &lt;code&gt;jobId&lt;/code&gt;, &lt;code&gt;jobIdExt&lt;/code&gt;. Check &lt;code&gt;success&lt;/code&gt; and &lt;code&gt;warnings&lt;/code&gt; before you trust anything else in the payload; a response with &lt;code&gt;fallbackUsed: true&lt;/code&gt; is telling you the extraction took a lower-confidence path on at least one field.&lt;/p&gt;

&lt;p&gt;A roughly shaped (not literal) version of what comes back looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deed of Trust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lenderName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Lending Co."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"loanAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;285000.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interestRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"loanTerm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"borrowerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane A. Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propertyAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"412 Elm Street, Springfield"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appraisedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;310000.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"closingCosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6420.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cashToClose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14200.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settlementAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Title &amp;amp; Escrow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"closingDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"comparableSales"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"398 Elm Street"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"salePrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;305000.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"saleDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-02"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectedPayments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"principalAndInterest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1732.21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mortgageInsurance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"estimatedEscrow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;410.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"estimatedTotalMonthlyPayment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2142.21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallbackUsed"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobIdExt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ext-..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is well over forty named fields spanning seven real-world categories a loan file actually has, returned from one call instead of built field by field in a mapping layer. One flag worth knowing before you quote a number in your own docs or a blog post: the n8n documentation page labels its own field list "30 total" while enumerating closer to fifty individual fields across those same categories. The discrepancy looks like a stale summary count on PDF4me's side rather than a real difference in what the parser returns, so pull one live response and count the fields yourself before repeating an exact number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Inputs That Change What You Get Back
&lt;/h2&gt;

&lt;p&gt;Two optional parameters are worth knowing before the first run. &lt;code&gt;documentType&lt;/code&gt; lets you tell the parser what kind of mortgage paperwork it is looking at, such as "Deed of Trust," "Promissory Note," "Mortgage Agreement," or "Loan Application," which the documentation says improves extraction accuracy. That makes sense: a promissory note and a title report share almost none of the same layout, so telling the parser which one it is looking at upfront removes a layer of guessing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;customFieldKeys&lt;/code&gt; goes further. It is an array of additional field names, beyond the standard schema above, that this specific pipeline needs. A servicer tracking an internal risk score or a regional disclosure code the standard schema does not cover can request it through this parameter rather than waiting on a schema update. That is a meaningfully different model from a rigid, fixed-output parser, and it is one of the more useful details buried in the request body rather than the headline feature list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Action, Four Ways In, Three Ways to Hand It a File
&lt;/h2&gt;

&lt;p&gt;The AI Mortgage Document Parser is not REST-only. It ships as a native action in &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-mortgage-document-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, a native module in &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-mortgage-document/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, a native node in &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-mortgage-document/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, and a native action in &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-mortgage-document-parser" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, with the same required inputs and the same output schema across all four.&lt;/p&gt;

&lt;p&gt;In Power Automate, the file comes in as binary content through &lt;code&gt;Mortgage Document File Content&lt;/code&gt;, mapped straight from a connector like SharePoint, OneDrive, Dropbox, or an email attachment, alongside the required &lt;code&gt;Mortgage Document Name&lt;/code&gt;, which matters for a loan-ops team that already lives in Microsoft 365 and wants the extracted fields to land directly in a SharePoint list or a Dynamics 365 record without a custom connector in between.&lt;/p&gt;

&lt;p&gt;n8n's node, named "AI-Process Mortgage Document," is the most explicit about this choice: an &lt;code&gt;Input Data Type&lt;/code&gt; parameter switches between &lt;strong&gt;Binary Data&lt;/strong&gt; (reads from a previous node's binary output via &lt;code&gt;Input Binary Field&lt;/code&gt;), &lt;strong&gt;Base64 String&lt;/strong&gt; (via &lt;code&gt;Base64 Mortgage Document Content&lt;/code&gt;), and &lt;strong&gt;URL&lt;/strong&gt; (via &lt;code&gt;Mortgage Document URL&lt;/code&gt;) and the filename always goes in separately through &lt;code&gt;Mortgage Document Name&lt;/code&gt;. Make exposes the same three input modes under its own field names. In Zapier, a mortgage PDF dropped into a watched folder can turn into a fully structured record in a CRM or spreadsheet with no code written at all.&lt;/p&gt;

&lt;p&gt;That consistency matters more than it sounds. A pipeline built once against this schema in Make can be rebuilt in n8n, or handed to a no-code teammate in Zapier, without anyone re-learning what &lt;code&gt;closingCosts&lt;/code&gt; or &lt;code&gt;settlementAgent&lt;/code&gt; means, because the field names do not change between platforms. The parser itself reportedly handles both scanned and digitally generated documents without altering the source file, which is the detail that actually matters if the original has to go into a compliance archive untouched. That handling claim comes from PDF4me's own integration documentation rather than from independently observed test output in this piece, so treat it as the vendor's stated behavior pending your own verification against a real scanned file.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Fixed Schema Is Not Enough
&lt;/h2&gt;

&lt;p&gt;The Mortgage Document Parser works because its schema is already built for one document family. But not every document a developer needs to parse is a mortgage, and PDF4me's broader AI lineup reflects that: it sits alongside dedicated parsers for pay stubs, tax documents, receipts, and purchase or sales orders, each with its own fixed schema tuned to that document type. For anything outside those categories, there is a second mechanism worth knowing about: the generic &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-parse" rel="noopener noreferrer"&gt;AI Document Parser&lt;/a&gt;, which runs against an Analyzer you define yourself in the PDF4me dashboard, with your own field names, types, and descriptions, called by an &lt;code&gt;AnalyzerId&lt;/code&gt; instead of a fixed schema. Edit that schema later and every workflow pointed at the same Analyzer ID picks up the change automatically, with no Zap or flow to rebuild. Choosing between the two is really a question of fit: reach for the Mortgage Document Parser when the document family is already mortgage paperwork and the standard fields cover the job, and reach for a self-defined Analyzer when the document type is something the standard parsers were never built for.&lt;/p&gt;

&lt;p&gt;Worth noting for anyone looking for a plain REST endpoint: there is no dedicated &lt;code&gt;pdf4me-api&lt;/code&gt; page for this specific parser outside the four integration platforms above (confirmed as a 404 on the expected path while researching this piece). If that changes, it will show up alongside the other &lt;code&gt;pdf4me-api&lt;/code&gt; endpoints in the &lt;a href="http://docs.pdf4me.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;If a lending, servicing, or proptech pipeline currently has a human opening PDFs to copy numbers into a loan system, this is the kind of task that disappears first. One call, one of four platforms, and a loan amount, a closing date, and a borrower name that were never typed by hand.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Would an API Parse Marriage Certificates? Inside PDF4me's Government-Document Parser</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:57:21 +0000</pubDate>
      <link>https://dev.to/pdf4me/why-would-an-api-parse-marriage-certificates-inside-pdf4mes-government-document-parser-dpo</link>
      <guid>https://dev.to/pdf4me/why-would-an-api-parse-marriage-certificates-inside-pdf4mes-government-document-parser-dpo</guid>
      <description>&lt;h1&gt;
  
  
  Why Would an API Parse Marriage Certificates? Inside PDF4me's Government-Document Parser
&lt;/h1&gt;

&lt;p&gt;Look at PDF4me's list of pre-built AI document parsers and eleven of them make instant sense. Invoices, bank statements, bank cheques, contracts, credit cards, health cards, pay stubs, tax documents, purchase orders: these are the documents every finance, ops, or back-office team drowns in daily. Then there's number seven on the list: a dedicated AI Marriage Certificate Parser. Next to a pile of invoices and tax forms, that one looks like it wandered in from a different product.&lt;/p&gt;

&lt;p&gt;It didn't. A marriage certificate is a government-issued document with a fixed set of fields, issued in enormous volume, read by humans who then type what they see into a database by hand. That is exactly the problem every other parser on that list solves. The only thing unusual about this one is that most developers never think to ask whether an API can handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually needs to parse a marriage certificate by API?
&lt;/h2&gt;

&lt;p&gt;Not a wedding photographer. The real demand comes from the systems that have to verify a marriage happened before they'll do something else. A mortgage lender underwriting a joint application needs to confirm both borrowers are legally married before combining income. An insurer processing a beneficiary change or a dependent enrollment needs the same proof. HR and benefits platforms need it when an employee adds a spouse to health coverage. Immigration and visa processing needs it as supporting evidence. County clerk offices digitizing decades of paper archives need it just to make old records searchable. None of these are edge cases. They are recurring, document-heavy workflows that currently run on someone opening a PDF or a scanned image and retyping names, dates, and certificate numbers into a form.&lt;/p&gt;

&lt;p&gt;That retyping step is where the real cost sits, and it is the same cost PDF4me's other eleven parsers were built to remove. The marriage certificate just happens to live in government and vital-records workflows instead of finance ones.&lt;/p&gt;

&lt;p&gt;Think about how a mortgage underwriting team actually handles a joint application today. Someone opens the uploaded scan, squints at a certificate that might be forty years old, typed on a county clerk's letterhead that changed its layout twice since then, and copies a certificate number and two names into the loan origination system by hand. Multiply that by every joint application that month. It's the same bottleneck an AP team has with invoices, just wearing a different outfit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the parser actually extracts
&lt;/h2&gt;

&lt;p&gt;This isn't OCR with a label slapped on it. The Marriage Certificate Parser is a pre-tuned schema, one of twelve PDF4me maintains across its AI Document Parser line, so a developer never builds or trains an extraction template themselves. According to &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;PDF4me's AI products page&lt;/a&gt;, you pick the parser that matches your document type and get structured JSON back, with no template maintenance on your side.&lt;/p&gt;

&lt;p&gt;For marriage certificates specifically, the schema pulls more than thirty structured fields in a single call. A typical response includes fields like:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"certificateNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MC-2024-0198273"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dateOfMarriage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-06-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"placeOfMarriage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cook County, Illinois"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuingAuthority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cook County Clerk's Office"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spouse1FullName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jordan A. Reyes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spouse1BirthDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1991-03-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spouse2FullName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Taylor M. Chen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spouse2BirthDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1990-11-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"officiantName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rev. Marcus Doyle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"officiantLicenseNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OFF-88213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"witnesses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Priya Natarajan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Daniel Osei"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authenticityIssues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fraudIndicators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authenticityRecommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No tampering indicators detected"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That field set (certificate identifiers, both spouses' details, officiant information, witness names, and document verification signals) maps directly onto what's live and verified on PDF4me's own Make integration page for this parser, confirmed field by field: &lt;code&gt;certificateNumber&lt;/code&gt;, &lt;code&gt;dateOfMarriage&lt;/code&gt;, &lt;code&gt;placeOfMarriage&lt;/code&gt;, &lt;code&gt;issuingAuthority&lt;/code&gt;, &lt;code&gt;spouse1FullName&lt;/code&gt;, &lt;code&gt;spouse1BirthDate&lt;/code&gt;, &lt;code&gt;spouse2FullName&lt;/code&gt;, &lt;code&gt;spouse2BirthDate&lt;/code&gt;, &lt;code&gt;officiantName&lt;/code&gt;, &lt;code&gt;officiantLicenseNumber&lt;/code&gt;, and &lt;code&gt;witnesses&lt;/code&gt;, plus the optional &lt;code&gt;authenticityIssues&lt;/code&gt;, &lt;code&gt;fraudIndicators&lt;/code&gt;, and &lt;code&gt;authenticityRecommendation&lt;/code&gt; fields from the fraud check.&lt;/p&gt;

&lt;p&gt;There's also an optional authenticity check built into the same call. Toggle it on and the parser returns fraud indicators, tampering signals, and a recommendation alongside the extracted data, useful anywhere a forged or altered certificate would be a real liability, like loan underwriting or benefits fraud review. If a field can't be confidently read, the response includes warnings instead of silently guessing, so a low-confidence extraction doesn't masquerade as a clean one.&lt;/p&gt;

&lt;p&gt;That matters more for this document type than most. An invoice from any vendor roughly follows the same layout logic: a header, line items, a total. A marriage certificate does not. Formats vary by state, by country, by decade, by whether the record was typed, handwritten, or stamped. A regex pattern tuned for one county clerk's template breaks the moment a different jurisdiction's certificate shows up. That's precisely the kind of document where a pre-tuned, maintained schema earns its keep over a brittle, hand-rolled extraction script. Nobody wants to own a growing pile of jurisdiction-specific regex just to keep a benefits-enrollment pipeline running.&lt;/p&gt;

&lt;p&gt;The authenticity check is worth sitting with for a second, because it's doing something different from the extraction itself. Extraction answers "what does this certificate say?" The fraud and tampering signals answer a separate question: "should I trust what it says?" Those are two different risk surfaces, and most teams building their own extraction script only ever solve the first one. A hand-rolled parser will happily hand you a clean-looking certificate number off a certificate that's been altered, because nothing in a basic text-extraction pipeline is looking for tampering in the first place. Bundling both checks into one call means a lender or insurer gets a usable answer to both questions without standing up a second system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you'd actually wire this in
&lt;/h2&gt;

&lt;p&gt;PDF4me doesn't make you choose between a REST call and a no-code workflow tool. The same Marriage Certificate Parser is available as a native action across multiple surfaces, and you pick whichever fits the system you're already building in.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-marriage-certificate-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, it's a connector action that takes the certificate's file content and filename (PDF, PNG, JPG, or JPEG all work) straight from SharePoint, OneDrive, email, or Dropbox, no separate upload step required. On &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-marriage-certificate/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, the AI-Process Marriage Certificate module accepts the same file either as binary data, a Base64 string, or a public URL, and returns the JSON payload shown above, ready to drop into the next step of a scenario. On &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-marriage-certificate/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, the node slots into a workflow the same way, pulling binary data from a prior node or a public HTTPS URL and returning structured JSON plus any warning or fallback-extraction flags. And on &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-marriage-certificate-parser/" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, the same AI Marriage Certificate Parser action handles PDF or image input and returns the full field set, with that page explicitly calling out vital records management, identity verification, insurance claims processing, immigration and visa services, and healthcare record management as its use cases. That's about as direct a confirmation as you'll get that this isn't a novelty parser; it's built for exactly the workflows described above.&lt;/p&gt;

&lt;p&gt;In every one of those four surfaces, the input requirements are nearly identical: the file itself and its filename, so the parser can detect the format correctly. Authenticity verification is an optional toggle in all of them, not a separate product. That consistency is the point. Whether your team automates in a low-code tool or calls an endpoint directly from your own backend, you're hitting the same underlying schema and getting the same field set back, which means switching tools later doesn't mean re-learning the output shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing worth planning for before you wire it in
&lt;/h2&gt;

&lt;p&gt;A marriage certificate carries more personally identifiable information per page than most of the documents developers are used to feeding into an API: full names, birth dates, ages, home addresses, and a government-issued certificate number, all in one payload. That's not a reason to avoid the parser, it's a reason to think about where the extracted JSON lands once the call returns. If your workflow writes the response straight into a loan file, an HR record, or a benefits database, that's exactly where this data is supposed to end up. If it's getting logged, cached, or passed through an intermediate step for debugging, it's worth treating that output the same way you'd treat any other field full of names and birth dates in your system, rather than assuming it's just another JSON blob because it came back from a document parser instead of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that's easy to miss
&lt;/h2&gt;

&lt;p&gt;The interesting lesson here isn't really about marriage certificates. It's that "document AI" has quietly stopped being synonymous with "invoice AI." The moment a document is structured, government-issued, high-volume, and currently processed by a human retyping fields into a system, it's a candidate for this kind of parser, whether that document is a certificate, a cheque, a pay stub, or something your team hasn't thought to automate yet. The marriage certificate parser looking out of place on that product page is actually the tell: if PDF4me built a dedicated, pre-tuned schema for something this specific, it's worth checking whether a similarly "unglamorous" document in your own pipeline already has one too, rather than assuming extraction tooling stops at finance paperwork.&lt;/p&gt;

&lt;p&gt;If you're weighing whether to build your own extraction logic for a government or vital-records document versus reaching for a parser that already exists, the honest comparison is maintenance, not just the first integration. A pre-tuned schema that PDF4me keeps current across jurisdictions and formats is a very different ongoing cost than a regex library your own team has to keep patching every time a new certificate layout shows up.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Does an Insurance Card Parser Return? Member ID, Plan, and Rx Data Explained</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Mon, 05 Oct 2026 18:28:51 +0000</pubDate>
      <link>https://dev.to/pdf4me/what-does-an-insurance-card-parser-return-member-id-plan-and-rx-data-explained-fng</link>
      <guid>https://dev.to/pdf4me/what-does-an-insurance-card-parser-return-member-id-plan-and-rx-data-explained-fng</guid>
      <description>&lt;p&gt;Hand a health insurance card to a generic OCR tool and you will get a transcript: every character on the card, in roughly the order it appeared, with no idea which string is the member ID and which is the group number. That distinction matters enormously in a benefits-verification or claims-intake pipeline, where the next system in line needs &lt;code&gt;memberId&lt;/code&gt; in one field and &lt;code&gt;rxBin&lt;/code&gt; in another, not a wall of text it has to re-parse itself. So what does a purpose-built insurance card parser actually hand back, field by field, and how do you build on top of it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Transcript Is Not the Same Thing as Structured Coverage Data
&lt;/h2&gt;

&lt;p&gt;Insurance cards are dense by design. A single card can carry a member's identity, their plan classification, their group affiliation, a coverage start date, and a separate set of prescription routing codes that have nothing to do with medical coverage at all. None of that is labeled consistently across insurers. One carrier prints "Member ID" top left; another buries it under a barcode. One spells out "Rx BIN" in full; another abbreviates it to three characters next to a logo. A generic OCR engine reads all of that correctly as text and still leaves you the job of figuring out which string means what, for however many insurer layouts your intake pipeline has to handle.&lt;/p&gt;

&lt;p&gt;That is the specific problem PDF4me's &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;AI Health Card Parser&lt;/a&gt; is built to close. It does not stop at reading characters. It understands the structure of a health insurance card well enough to return member identity, plan details, and prescription routing data as named fields, regardless of which insurer issued the card or how that insurer chose to lay it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Documented Inputs
&lt;/h2&gt;

&lt;p&gt;The action's own &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-health-card-parser/" rel="noopener noreferrer"&gt;Power Automate documentation&lt;/a&gt; lists exactly three inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health Card File Content&lt;/strong&gt; (binary, required): the card as a PDF, PNG, JPG, or JPEG, typically mapped from a prior "Get file content" step, SharePoint, OneDrive, or an email attachment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Card Name&lt;/strong&gt; (string, required): the filename, including its extension, used for format detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;customFieldKeys&lt;/strong&gt; (array, optional, under Advanced parameters): additional field names to extract beyond the standard health card data set, for insurers whose cards carry something extra you need captured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-health-card/" rel="noopener noreferrer"&gt;Make&lt;/a&gt; and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-health-card/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; expose the same three inputs under their own node UI, since all three wrap the same underlying parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Exact Response Shape
&lt;/h2&gt;

&lt;p&gt;This is the response example from the Power Automate documentation, quoted verbatim rather than reconstructed from memory, since marketing-page JSON examples elsewhere in this product line have been known to omit fields the real payload sends:&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Health card processed successfully using AI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processedData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"healthData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"memberId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"groupNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GRP001234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"planType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PPO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"insuranceProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Blue Cross Blue Shield"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"memberName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Michael Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dateOfBirthStr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1985-03-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"effectiveDateStr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxBin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxPcn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABC123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rxGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RXGRP001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"copayInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Primary Care"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"copay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$25"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Specialist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"copay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$50"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Emergency Room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"copay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$200"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"memberId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"memberName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"insuranceProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11111111-2222-3333-4444-555555555555"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processingTimestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15T10:30:45.123Z"&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;Ten named fields under &lt;code&gt;healthData&lt;/code&gt;, plus the &lt;code&gt;copayInfo&lt;/code&gt; array, nested inside &lt;code&gt;processedData&lt;/code&gt;, wrapped with a top-level &lt;code&gt;success&lt;/code&gt; flag and a &lt;code&gt;jobId&lt;/code&gt; for tracking. Note that the sample's &lt;code&gt;confidence&lt;/code&gt; block only lists three of the ten fields explicitly; treat that as illustrative rather than exhaustive, and check for a confidence value on every field your flow actually depends on before trusting it blankly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing the Response: Branch on Confidence, Don't Trust It Blankly
&lt;/h2&gt;

&lt;p&gt;The field worth writing real code around is &lt;code&gt;confidence&lt;/code&gt;, not just &lt;code&gt;healthData&lt;/code&gt;. A card with a crisp, high-resolution member ID and a slightly smudged group number can come back with a confidence near 1.0 on the first field and meaningfully lower on the second, in the same response. Here is a small Python helper that takes the parsed JSON above and splits fields into "safe to auto-process" and "needs a human" based on a threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;split_by_confidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;health_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_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;processedData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;health_data&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;confidence&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;auto_processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;needs_review&lt;/span&gt; &lt;span class="o"&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;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;health_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copayInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;confidence&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;field&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;score&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# No confidence reported for this field -- treat as needing review
&lt;/span&gt;            &lt;span class="n"&gt;needs_review&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;auto_processed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;needs_review&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&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;auto_processed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needs_review&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that into a Power Automate, Make, or n8n flow and the branch becomes mechanical: everything in &lt;code&gt;auto_processed&lt;/code&gt; writes straight to the benefits record, everything in &lt;code&gt;needs_review&lt;/code&gt; goes to a queue a human glances at, one field at a time, instead of the whole card getting rejected over a single low-confidence value. That is the difference between a parser that looks good in a demo and one that survives contact with a real intake pipeline, where card quality varies and insurers don't agree on layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Beats Building a Template for Every Insurer
&lt;/h2&gt;

&lt;p&gt;The honest alternative to a pre-built parser is building a template, or a set of regex patterns, for every insurer layout your pipeline needs to handle, then maintaining that set as insurers redesign their cards. That works until the third or fourth carrier, at which point the maintenance burden starts to outweigh the time saved by automating in the first place. The AI Health Card Parser is pre-tuned specifically so that step never has to happen: there is no template to create and no field positions to configure before the first card goes through it.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what kind of parser this is, because PDF4me ships more than one shape of AI extraction. The Health Card Parser is a pre-built, pre-tuned extractor with a fixed schema: send a card, get back the fields above, nothing to configure first. That's different from PDF4me's generic &lt;a href="https://docs.pdf4me.com/general-guidelines/ai-document-parser-using-parse" rel="noopener noreferrer"&gt;AI Document Parser using a custom Document Schema&lt;/a&gt;, where you define your own JSON schema and field list in the dashboard for document types that don't have a dedicated pre-built parser yet. If your document is a health insurance card, the pre-built parser is the faster and more accurate path; the custom schema route exists for everything outside PDF4me's current catalog of pre-built parsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Worked Scenario: Telehealth Intake
&lt;/h2&gt;

&lt;p&gt;Consider a telehealth platform that asks a new patient to photograph the front of their insurance card during signup. Before a parser like this existed, that photo either sat in storage until an intake coordinator manually typed the member ID and group number into the practice management system, or the platform skipped verification entirely and found out the coverage details were wrong only when a claim bounced back weeks later.&lt;/p&gt;

&lt;p&gt;Run that photo through the parser instead: &lt;code&gt;memberId&lt;/code&gt;, &lt;code&gt;memberName&lt;/code&gt;, and &lt;code&gt;dateOfBirthStr&lt;/code&gt; populate the patient record directly. &lt;code&gt;planType&lt;/code&gt; and &lt;code&gt;insuranceProvider&lt;/code&gt; determine which eligibility check runs next, since a PPO and an HMO often route through different downstream verification steps. &lt;code&gt;rxBin&lt;/code&gt;, &lt;code&gt;rxPcn&lt;/code&gt;, and &lt;code&gt;rxGroup&lt;/code&gt;, if the visit involves a prescription, hand off straight to the pharmacy benefits check, separately from the medical coverage check, because those two checks genuinely don't share a code path. Pair every one of those handoffs with the &lt;code&gt;split_by_confidence&lt;/code&gt; logic above, and a low-confidence &lt;code&gt;groupNumber&lt;/code&gt; gets flagged for a quick human glance instead of silently feeding a wrong value into an eligibility check that fails for reasons nobody can trace back to the original card photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Drops Into an Existing Flow
&lt;/h2&gt;

&lt;p&gt;Because the Health Card Parser is a native action in &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-health-card-parser/" rel="noopener noreferrer"&gt;Power Automate&lt;/a&gt;, &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-health-card/" rel="noopener noreferrer"&gt;Make&lt;/a&gt;, and &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-health-card/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;, it slots into an existing intake flow rather than requiring a new one built around it. A Power Automate flow can take an uploaded card straight from a SharePoint library or a Forms submission; a Make scenario can start from a webhook trigger; an n8n workflow can sit inside a larger patient-onboarding chain next to whatever CRM or EHR integration already exists. None of the three requires writing a schema first, because the schema is already built into the parser itself.&lt;/p&gt;

&lt;p&gt;That's the trade this specific parser makes against PDF4me's more general, schema-driven Document Parser: less flexibility for documents that don't fit the health card mold, in exchange for zero setup time for the ones that do, and a response shape you can start writing confidence-aware logic against the moment the first card comes through.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>ai</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Extract Card Number, Expiry, and Cardholder Name from a Credit Card Image via API</title>
      <dc:creator>PDF4me</dc:creator>
      <pubDate>Thu, 01 Oct 2026 15:41:37 +0000</pubDate>
      <link>https://dev.to/pdf4me/how-to-extract-card-number-expiry-and-cardholder-name-from-a-credit-card-image-via-api-hjm</link>
      <guid>https://dev.to/pdf4me/how-to-extract-card-number-expiry-and-cardholder-name-from-a-credit-card-image-via-api-hjm</guid>
      <description>&lt;h1&gt;
  
  
  How to Extract Card Number, Expiry, and Cardholder Name from a Credit Card Image via API
&lt;/h1&gt;

&lt;p&gt;Somewhere in your company right now, someone is looking at a photo of a credit card and typing the number into a spreadsheet by hand. Maybe it's a reimbursement form. Maybe it's a KYC check during account setup. Maybe it's a vendor onboarding packet where a scanned card sits next to a dozen other documents nobody wants to open individually. Whatever the reason, that number gets retyped, and retyping sixteen digits by hand is exactly the kind of task that produces a transposed digit on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;PDF4me's AI Credit Card Parser exists to take that step out of the loop entirely. Point it at a credit card image, scan, or PDF, and it returns the card number, expiry date, cardholder name, and brand as structured, mappable fields, ready to drop into whatever system is waiting downstream. It's one of twelve pre-tuned document parsers PDF4me ships on its &lt;a href="https://pdf4me.com/products/ai" rel="noopener noreferrer"&gt;AI products page&lt;/a&gt;, sitting alongside parsers for bank statements, insurance cards, mortgage documents, and tax forms. Each one is purpose-built for a document type people deal with constantly and rarely enjoy processing manually.&lt;/p&gt;

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

&lt;p&gt;The core extraction is consistent across every platform PDF4me supports it on: card number, expiry date, cardholder name, and card type or brand (Visa, Mastercard, and so on). From there, the field list gets more generous depending on which integration you're using. The &lt;a href="https://docs.pdf4me.com/integration/power-automate/pdf4me-ai/ai-credit-card-parser/" rel="noopener noreferrer"&gt;Power Automate action&lt;/a&gt; adds issuing bank, an associated account number, and valid-from and valid-through date ranges. The &lt;a href="https://docs.pdf4me.com/integration/make/pdf4me-ai/ai-process-credit-card/" rel="noopener noreferrer"&gt;Make module&lt;/a&gt; and the &lt;a href="https://docs.pdf4me.com/integration/zapier/pdf4me-ai/ai-credit-card-parser/" rel="noopener noreferrer"&gt;Zapier action&lt;/a&gt; both extract a CVV field too, when one is visible on the document, alongside the same bank and account details. The &lt;a href="https://docs.pdf4me.com/integration/n8n/pdf4me-ai/ai-process-credit-card/" rel="noopener noreferrer"&gt;n8n node&lt;/a&gt; returns everything inside a single &lt;code&gt;creditCardData&lt;/code&gt; object rather than a flat field list, which is worth knowing before you start mapping variables in a workflow.&lt;/p&gt;

&lt;p&gt;Here's roughly what that structured output looks like once it lands in your workflow, assembled from the field names documented across Power Automate, Make, n8n, and Zapier (illustrative shape, not a copy-pasted sample payload):&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cardNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4111 1111 1111 1111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiryDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"09/29"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cardholderName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"J SAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cardType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Visa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cardBrand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Visa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuingBank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Bank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accountNumber"&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;"validFrom"&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;"validThru"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"09/29"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallbackUsed"&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;Every version of the extraction ships with a quality layer worth paying attention to. Alongside the fields themselves, the response includes a &lt;code&gt;warnings&lt;/code&gt; array flagging anything the parser wasn't confident about, and a &lt;code&gt;fallbackUsed&lt;/code&gt; boolean telling you whether it had to fall back to a secondary extraction method to get a result. If a field genuinely can't be read off the image, n8n's documentation specifically notes it comes back as an empty string rather than a missing key, which matters if your downstream logic checks for the field's presence instead of its value.&lt;/p&gt;

&lt;p&gt;There's also a &lt;code&gt;customFieldKeys&lt;/code&gt; parameter available across Power Automate, Make, n8n, and Zapier. It's the escape hatch for anything the standard field set doesn't cover, letting you ask the parser to also look for a loyalty number, an internal reference code, or whatever else happens to appear on the card alongside the standard fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't a payment processor, and why that matters
&lt;/h2&gt;

&lt;p&gt;Here's the part worth reading twice before you wire this into anything customer-facing: PDF4me's own documentation is explicit that this tool "does not process transactions and carries no PCI DSS certification." It reads a card image and gives you back structured text. It does not authorize a charge, store a card on file for future billing, or meet the compliance bar that actual payment processing requires.&lt;/p&gt;

&lt;p&gt;That's not a knock on the feature. It's a scope boundary, and a sensible one. A document parser and a payment gateway solve different problems, and conflating them is how companies end up with compliance headaches nobody budgeted for. If your workflow needs to actually charge a card, this sits upstream of that, feeding a real payment processor or your existing PCI-compliant systems, never replacing them.&lt;/p&gt;

&lt;p&gt;What this does solve well is everything adjacent to the actual transaction. Expense reports where an employee photographs a receipt and a card. Vendor onboarding where a scanned document needs its payment details logged for internal records. Reconciliation workflows where someone needs to match a statement line to a card type without opening the statement themselves. None of those require PCI scope. All of them currently involve someone typing numbers from a screen into a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into your automation platform of choice
&lt;/h2&gt;

&lt;p&gt;The shape of the workflow is nearly identical no matter which platform you're building on, which is worth knowing if your team runs more than one of them. You need two inputs: the file itself (as binary data, a base64 string, or in some integrations a public URL) and the filename with its extension intact, since PDF4me uses that extension to pick the right processing path. Supported formats are PDF, PNG, JPG, and JPEG across every platform.&lt;/p&gt;

&lt;p&gt;In Power Automate, that means a File Content field and a filename parameter feeding straight into the AI Credit Card Parser action, with the structured output available as dynamic content for whatever comes next, routing it into a SharePoint list, a Dataverse table, or an approval flow. In Make, the module accepts binary data pulled straight from a Dropbox or Google Drive watch, a base64 string, or a public URL, and the resulting &lt;code&gt;creditCardData&lt;/code&gt; object comes back as individually mappable tokens you can route anywhere in the scenario, including validation steps before anything touches a payment or ERP system. The Zapier action works the same way with a Credit Card File and Credit Card Name as its required inputs, which plugs neatly into a Zap that starts with a new email attachment or a form upload. And the n8n node wraps the whole thing into one action step, taking your PDF4me API credential and the same input options, then handing back &lt;code&gt;creditCardData&lt;/code&gt; plus a &lt;code&gt;jobId&lt;/code&gt; and &lt;code&gt;jobIdExt&lt;/code&gt; you can use for tracking if the workflow needs to check on a longer-running extraction later.&lt;/p&gt;

&lt;p&gt;None of these require you to write a parsing library, train a model on card layouts, or maintain regex patterns for every card network's formatting quirks. You're wiring an action into a trigger, not building an extraction pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the fields you shouldn't keep
&lt;/h2&gt;

&lt;p&gt;Getting a full card number back as clean, structured text is useful and also exactly the kind of thing that should make you pause before deciding what to do with it next. The responsible default is to treat the raw card number and any CVV field as transient: use them for the one step that needs them, mask or drop them immediately after, and store only what you actually need long-term, which for most internal workflows is a last-four reference and the card brand, not the full number.&lt;/p&gt;

&lt;p&gt;PDF4me's own n8n documentation puts this plainly, advising teams to drop sensitive fields immediately rather than storing full card data at rest. That's good practice regardless of which platform you're on. If your automation writes the extracted fields into a spreadsheet, a CRM, or a database table, that's the moment to decide what actually needs to persist versus what was only ever needed transiently to complete one step in the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually fits
&lt;/h2&gt;

&lt;p&gt;The honest use case here isn't "replace your payment processor." It's closer to "stop making a human retype sixteen digits from a photo." Expense management tools that currently ask employees to manually enter card details alongside a receipt. Onboarding workflows that collect a scanned document and need its payment reference logged without anyone opening the file. Internal reconciliation processes where a card type and partial number need to get matched against a statement, and the rest of the data was never going to be used for anything beyond that match.&lt;/p&gt;

&lt;p&gt;If your team is already running Power Automate, Make, Zapier, or n8n for other document workflows, adding the Credit Card Parser is the same pattern you've already built elsewhere: a trigger, this action, and a destination for the structured result. It's worth testing against a handful of real card images and scans first, since the quality warnings and fallback flag exist precisely because card layouts, lighting, and image quality vary more than, say, a standardized invoice format does.&lt;/p&gt;

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

</description>
      <category>api</category>
      <category>automation</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
