Most document pipelines have a reflex. A PDF comes in, and the first instinct is: run OCR, then parse it. That reflex costs time and money on documents that never needed it in the first place.
Here's the distinction that gets skipped over. A PDF generated from Word, from an invoicing system, from a web page, from almost any modern software, is "born digital." Every character on the page is already stored as text, positioned and selectable, the same way this article's text is selectable in your browser. A scanned PDF is different: it's a photograph of a page, a grid of pixels with no text underneath it at all. OCR exists to solve that second problem. It reads the pixels and reconstructs a text layer that wasn't there. PDF OCR is PDF4me's endpoint for exactly that job, and its own documentation lists "Intelligent Processing: skip OCR when text is already searchable to optimize performance" as a named feature, which is the whole thesis of this article in one line.
But if the PDF already has a text layer, running it through OCR first is a wasted step: extra processing time, extra cost, extra room for OCR to introduce recognition errors into text that was already perfect. A large share of the PDFs moving through business automation, generated invoices, exported reports, system-generated confirmations, contracts drafted in Word and exported to PDF, are born digital from the start. They don't need OCR. They need something that can read the text layer that's already there and pull out exactly the values that matter.
That's what Extract Text by Expression does.
One regex, one endpoint
POST https://api.pdf4me.com/api/v2/ExtractTextByExpression
No OCR step. No AI model. No template you have to build in a dashboard first. The request is small:
| Parameter | Type | Required | Description |
|---|---|---|---|
docContent |
Base64 String | Yes | The source PDF, Base64-encoded |
docName |
String | Yes | Filename with .pdf extension |
expression |
String | Yes | A standard regular expression: groups, quantifiers, and anchors all supported |
pageSequence |
String | Yes | Which pages to search: "1-" for all pages, "1-3" for a range, "1,2,3" for specific pages |
async |
Boolean | No |
true returns 202 Accepted with a Location header to poll instead of blocking |
There's no separate anchor-text mode and no capture areas to draw. You write one pattern, point it at the pages that matter, and send the call. A reference number in a known format, an email address, a date pattern, an order ID with a fixed prefix and variable digits, a line that always starts with "Total Due: " followed by a dollar figure: all of these are expressible as a single regex pattern, no dashboard-configured template required.
On success, the response is just as minimal: a textList, a flat JSON array of the strings that matched, in the order they were found. No per-match position data, no capture-name labels. If a downstream step needs to know exactly where on the page a match lives, or needs several differently-labeled fields pulled from one document in a single call, that's the signal this endpoint has reached the edge of what it's built for (more on that below).
What the call actually looks like
PDF4me publishes a working Python sample in its pdf4me-api-samples repository (MIT licensed). Trimmed down to the core request/poll cycle, adapted for this article:
import base64
import requests
import time
api_key = "YOUR_API_KEY" # https://dev.pdf4me.com/dashboard/#/api-keys/
url = "https://api.pdf4me.com/api/v2/ExtractTextByExpression"
with open("invoice.pdf", "rb") as f:
pdf_base64 = base64.b64encode(f.read()).decode("utf-8")
payload = {
"docContent": pdf_base64,
"docName": "invoice.pdf",
"expression": r"INV-\d{6,10}", # regex pattern to match
"pageSequence": "1-", # all pages
"async": True
}
headers = {
"Authorization": f"Basic {api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers, timeout=300)
if response.status_code == 200:
matches = response.json()["textList"]
elif response.status_code == 202:
# Async: poll the Location header until it returns 200
location = response.headers["Location"]
for _ in range(15):
time.sleep(8)
poll = requests.get(location, headers=headers, timeout=60)
if poll.status_code == 200:
matches = poll.json()["textList"]
break
elif poll.status_code != 202:
raise RuntimeError(f"Extraction failed: {poll.status_code} {poll.text}")
else:
raise RuntimeError(f"Request failed: {response.status_code} {response.text}")
print(matches) # ["INV-2024001", "INV-2024002", ...]
Two things worth calling out from the official sample: it always sends "async": True and handles both the immediate 200 and the polling 202 path, since PDF4me's own docs don't specify a file-size threshold where sync stops being appropriate; and the sample repo's own request calls verify=False (disabling TLS certificate verification), which is dropped here rather than carried into production code, don't disable certificate verification in anything that ships.
Where this sits between OCR and full parsing
It helps to place this endpoint on a spectrum, because PDF4me ships tools at more than one level of sophistication for a reason, and reaching for the heaviest one by default is as wasteful as running OCR on a document that doesn't need it.
At one end, OCR turns pixels into text. You only need it when the document is scanned and has no text layer at all. If a document does come in scanned and it has predictable structure, the workflow is OCR first, then Extract Text by Expression (or a parsing tool) on the result.
At the other end sits Parse Document, which runs a saved template, configured once in the PDF4me dashboard, against a PDF and returns a structured response keyed by the field names you defined. That template can still use a regex expression under the hood for stable patterns, or a JavaScript expression for conditional logic and classification, but the template itself is reusable across calls and platforms by a stable TemplateId, which is exactly what you want when a single logical document type, invoices, say, shows up across many different actual layouts from different vendors, where one fixed pattern won't reliably survive the variation. PDF4me's own documentation draws the line plainly: Extract Text by Expression is described as the "lower-level endpoint that runs a single regex against a PDF without a saved template," useful for ad-hoc extraction, while Parse Document is the template-based path for anything that needs to run repeatedly and consistently.
Extract Text by Expression sits in the middle, and for a specific kind of document, it's the right tool, not the compromise one. If your documents are generated by your own systems or by a small number of predictable sources, an invoice template your own billing software produces, a confirmation email rendered to PDF, a government form with a fixed layout, the text around the value you want doesn't change from one document to the next. That's exactly the condition under which a single regex pattern is reliable, fast, and requires no template setup, no training data, and no inference cost. The honest tradeoff: the moment layout variation creeps in and the surrounding wording changes between vendors, a fixed pattern gets brittle fast, and that's the signal to move up to Parse Document instead.
Built to run inside the automation you already have
This isn't a REST-only capability. PDF4me exposes Extract Text by Expression as a native module or action across every major automation platform: Make, Power Automate, Zapier, and n8n. In each of them the shape is the same: feed in a PDF, define the pattern, get matched values back as fields you can map into the next step, a database write, a CRM update, a conditional branch on whatever value came back.
Before wiring any of this into a production flow, it's worth testing the exact pattern against a real document first. PDF4me's API Tester lets you try Extract Text by Expression directly in the browser, no code, so you can confirm a regex pattern actually matches before you build automation around it. And for the REST path specifically, Connect to the PDF4me V2 API covers the base URL, authentication headers, and request and response format you'll need regardless of which pattern you're sending.
The honest limits
This is not a document-understanding tool, and it doesn't try to be one. It has no concept of what a "total" or a "date" means, it only knows the literal pattern you told it to match. Feed it a document where the regex doesn't match the actual formatting, and you get an empty textList back, not a best guess. That's a feature for anyone who has been burned by an extraction tool confidently returning a wrong number. It's also a real limitation for anyone hoping one pattern will survive a document set with more layout variety than they first assumed. Know which kind of document set you're actually dealing with before choosing this over Parse Document, and the choice mostly makes itself.
Website: pdf4me.com
Documentation: docs.pdf4me.com
Developer portal: dev.pdf4me.com
Top comments (0)