Every document pipeline has a fork near the front of it. A file lands in a folder or an inbox, and something has to decide what kind of file it is before anything useful can happen to it. Invoices go to accounts payable. Contracts go to legal review. Receipts go to expense matching. Everything else goes to a human.
Most teams solve this twice. The first attempt reads the filename, and it holds up until a vendor changes their export settings and every file starts arriving as a timestamp. The second attempt searches the extracted text for a keyword, and it holds up until a contract mentions the word invoice in a payment terms clause.
The durable fix is a classification step: one call that takes the document and tells you what it is, before anything downstream commits to parsing it. PDF4me exposes that as Classify Document. This post covers the request, what comes back, and the one verification step worth doing before you write the branch that consumes it.
Two different things are called classification here
Worth being clear about this up front, because it changes what lands in your response.
There is content-based classification, where you send a document and PDF4me analyses its structure, content patterns and metadata to identify a type. Nothing to configure in advance.
And there is rules-based classification, where you define your own classes in the PDF4me dashboard and attach a matching expression to each one. Document Classification for PDFs walks through that setup, and the saved rules are used by the REST API and by the Zapier, Make, n8n and Power Automate steps alike.
Both run through the same endpoint. Which behaviour you are effectively getting depends on how your account is configured, which is the reason for the verification step later in this post.
The request
The endpoint is a single POST. Two fields are required, one is optional.
| Parameter | Type | Required | Notes |
|---|---|---|---|
docContent |
Base64 string | Yes | The document, Base64 encoded |
docName |
String | Yes | Source file name with its extension |
async |
Boolean | No | When true, returns 202 Accepted with a Location header to poll |
Here is the call in Python, adapted from the official pdf4me-api-samples repository, which is MIT licensed.
import base64
import json
import requests
API_KEY = "your_pdf4me_api_key"
URL = "https://api.pdf4me.com/api/v2/ClassifyDocument"
with open("sample.pdf", "rb") as f:
doc_content = base64.b64encode(f.read()).decode("utf-8")
payload = {
"docContent": doc_content,
"docName": "sample.pdf",
"async": True,
}
headers = {
"Authorization": f"Basic {API_KEY}",
"Content-Type": "application/json",
}
response = requests.post(URL, json=payload, headers=headers, timeout=300)
print(response.status_code)
One deliberate change from the published sample. The repository version passes verify=False to requests, which disables TLS certificate verification. That is convenient behind a corporate proxy and a bad default to copy into production, so it is removed above. If you hit certificate errors, fix the certificate chain rather than turning the check off.
The API key goes in an Authorization: Basic header. Connect to the PDF4me V2 API covers the base URL and header format, and keys come from the dashboard.
What comes back
The response is JSON. Here is the success shape documented on the Classify Document API page.
{
"documentType": "invoice",
"category": "financial",
"confidence": 0.95,
"metadata": {
"pageCount": 1,
"createdDate": "2024-01-15T10:30:00Z"
}
}
Two details in that example are easy to skim past and expensive to get wrong.
The values are lowercase. It is "invoice", not "Invoice". If your router does a case-sensitive equality check, that single detail decides whether the branch fires.
And the example on that page carries a single prediction with no alternatives array. That is not universal though. The n8n node documents an alternativeClassifications field in its output, so ranked candidates do exist on at least one surface. Which behaviour you get is one more thing that varies by where you call from.
Now the part that matters more than either. When you are running your own saved classes, the field you care about is className, the name you gave the class yourself. The dashboard setup guide documents the response that way, and the official Python sample's README lists a wider set again, including templateId, templateName, className, traceId and subscriptionUsage.
The sample code itself is the tell. It commits to no field names at all.
classification_data = response.json()
if isinstance(classification_data, dict):
for key, value in classification_data.items():
print(f" {key}: {value}")
That is a sensible way to write a sample, and it is also a hint worth taking. The exact key set depends on how your account is configured, so the field list to code against is the one your own account returns, not the one printed in any article, this one included.
Confirm the shape before you write the branch
This is the two minute step that saves the afternoon.
Send one of your own real documents through the interactive API tester for Classify Document, read the response that actually comes back for your account, and build against that. It settles the exact key set, the exact casing, whether your document type is recognised at all, and what a realistic score looks like for the documents you genuinely process rather than for a clean sample.
Classification is a decision point, and decision points are worth confirming by hand once before ten thousand documents flow through them unattended.
What to do with a confidence value when you get one
Where a confidence value is present, it is the most useful thing in the response, and it is also the thing most pipelines waste.
A classifier that returns only a label forces your code into a binary posture. The label is either right or it is wrong, and you find out which downstream, usually when a parser returns nulls for every field. A label plus a score lets you build a third branch: route it, reject it, or set it aside for a person.
That third branch is where the value sits. What you should not do is copy a threshold out of an article, including this one. There is no universal cut-off, because the cost of being wrong is not constant. Misrouting a receipt into the wrong expense category costs somebody five minutes. Misrouting a signed contract into a parser that flattens it to key-value pairs costs considerably more. Set the threshold against the cost of a bad route in your pipeline, log the scores you see in production for a few weeks, then adjust from evidence.
One habit worth building early: store the score alongside the routing decision in your system of record. When somebody asks six months from now why a document went where it went, a stored score turns an argument into a lookup.
Large documents and the async path
Set async to true and the API returns 202 Accepted with a Location header instead of a result, plus a trace identifier in the body. You then poll that URL until it returns 200.
if response.status_code == 202:
location_url = response.headers.get("Location")
for attempt in range(20):
time.sleep(15)
poll = requests.get(location_url, headers=headers, timeout=60)
if poll.status_code == 200:
print(json.dumps(poll.json(), indent=2))
break
The retry budget above matches the official sample, twenty attempts at fifteen second intervals. Tune it to your own documents rather than treating it as a constant.
Defining your own classes
If your documents come from a known set of vendors, rules beat prediction. A vendor template that reliably carries a fixed string on page one is a deterministic signal, and deterministic is easier to debug and easier to explain to an auditor than a score.
You set this up in the PDF4me dashboard under Classify Document, described in Document Classification for PDFs. Each class gets a Class Name, which is the value returned when a document matches, and a Search Text expression, either a regular expression or JavaScript. The documented examples give you the flavour:
invoice(.*) matches "invoice" followed by anything
Invoice\s*#\s*Pdf4me-\d{6}-\d{5} matches a specific invoice number format
(.*) catch-all
The page also gives you Upload Template File, Select Test File and Test Classify controls, so you can check a rule against a real document and see the matched Class Name before saving. Use them. A regex that matches everything because you left a catch-all above a specific rule is a quiet failure, and the test panel finds it in seconds.
Worth noting that the documentation qualifies the JavaScript option with "if supported", so treat regular expressions as the well-trodden path and verify JavaScript behaviour for your account before depending on it.
The trade you are making here is coverage. The day a new vendor appears, a rules-based classifier has nothing to say about it, while a content-based one still produces a best guess you can gate on.
When classification is only half the job
If you were always going to extract fields after routing, the fork can disappear entirely. AI Document Parser using Classify describes building one analyzer that holds several schemas, one per document variant, each with a classification name, a classification prompt and a document schema. Each incoming file gets routed to the matching schema and its fields come back, in a single request.
When there is only one document shape to handle, AI Document Parser using Parse is the simpler build, and the standalone Parse Document endpoint covers schema-driven extraction on its own.
The same call without the code
None of this is REST only. Classify Document is exposed as a step in Make, Power Automate, Zapier and n8n, and the analyzer route is available too, for example through the AI Document Parser node in n8n. Classes you save in the dashboard apply to all of them, so the setup work is done once.
The workflow shape is the same everywhere: a trigger, a download step, the classify step, then a router or switch branching on the returned value. If you would rather follow a working build than assemble one from reference pages, there are step-by-step walkthroughs with screenshots for Make, Power Automate, Zapier and n8n.
One thing to check on your platform of choice before building the branch: which fields the step actually surfaces to the canvas. A value that exists in the JSON is not automatically mapped into a variable you can reference in a condition.
The short version
One POST with two required fields gives you a routing decision. The response carries a document type or your own class name depending on how the account is set up, so confirm the exact keys against your own account in the API tester before you write the branch. Where a confidence value is present, use it to build a third path for the uncertain cases instead of forcing every document into a yes or no.
That is the difference between a pipeline that sorts documents and a pipeline that quietly misfiles them.
Website: pdf4me.com
Documentation: docs.pdf4me.com
Developer portal: dev.pdf4me.com
Top comments (0)