curl --request POST \
--url https://api.runpulse.com/classify \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <api-key>' \
--form 'classify_config={
"classifications": {}
};type=application/json' \
--form file=@example-file \
--form 'file_url=<string>' \
--form 'page_range=<string>' \
--form async=false \
--form parent_job_id=3c90c3cc-0d44-4b50-8888-8dd25736052aimport requests
url = "https://api.runpulse.com/classify"
files = { "file": ("example-file", open("example-file", "rb")) }
files["classify_config"] = (None, "{\n \"classifications\": {}\n}", "application/json")
payload = {
"file_url": "<string>",
"page_range": "<string>",
"async": "false",
"parent_job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('classify_config', '{
"classifications": {}
}');
form.append('file', '<string>');
form.append('file_url', '<string>');
form.append('page_range', '<string>');
form.append('async', 'false');
form.append('parent_job_id', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
options.body = form;
fetch('https://api.runpulse.com/classify', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.runpulse.com/classify",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.runpulse.com/classify"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.runpulse.com/classify")
.header("x-api-key", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/classify")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"classification": "<string>",
"classify_output": {
"classification": "<string>",
"pipeline_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_range": "<string>",
"confidence": 0.5,
"scores": {}
},
"confidence": 0.5,
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credits_used": 123
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>"
}Classify Document
Lightweight routing step that runs before /extract. Given a raw document
(file upload or file_url) and a set of caller-defined classifications,
it returns which classification the document belongs to — plus that classification’s
pipeline_id, so you can send the document to the right pipeline next.
By default, it evaluates the first five pages. For PDFs and images,
billing is based on the effective page_range. For Office and HTML
files, billing applies to every page.
Billed at 0.5 credits per page — half the /extract rate.
Accepted document types are identical to /extract, including the same
size limits and URL validation.
Set async: true to return immediately with a job_id for polling via
GET /job/. Otherwise processes synchronously.
curl --request POST \
--url https://api.runpulse.com/classify \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <api-key>' \
--form 'classify_config={
"classifications": {}
};type=application/json' \
--form file=@example-file \
--form 'file_url=<string>' \
--form 'page_range=<string>' \
--form async=false \
--form parent_job_id=3c90c3cc-0d44-4b50-8888-8dd25736052aimport requests
url = "https://api.runpulse.com/classify"
files = { "file": ("example-file", open("example-file", "rb")) }
files["classify_config"] = (None, "{\n \"classifications\": {}\n}", "application/json")
payload = {
"file_url": "<string>",
"page_range": "<string>",
"async": "false",
"parent_job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('classify_config', '{
"classifications": {}
}');
form.append('file', '<string>');
form.append('file_url', '<string>');
form.append('page_range', '<string>');
form.append('async', 'false');
form.append('parent_job_id', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
options.body = form;
fetch('https://api.runpulse.com/classify', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.runpulse.com/classify",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.runpulse.com/classify"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.runpulse.com/classify")
.header("x-api-key", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/classify")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"classify_config\"\r\nContent-Type: application/json\r\n\r\n{\r\n \"classifications\": {}\r\n}\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"page_range\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"async\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"parent_job_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"classification": "<string>",
"classify_output": {
"classification": "<string>",
"pipeline_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_range": "<string>",
"confidence": 0.5,
"scores": {}
},
"confidence": 0.5,
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credits_used": 123
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>"
}Overview
/extract settings) a document should be routed to.POST /classify is a lightweight routing step that runs before /extract. Given a raw document and a set of caller-defined classifications, it returns which classification the document belongs to — plus that classification’s pipeline_id, so you can send the document to the right pipeline next.
Why classify before extract: extraction settings (chunking, schema, tables, …) differ per document type, so you need to know the type before you extract. By default, /classify evaluates the first five pages and costs 0.5 credits per page — half the /extract rate. See Credits.
Accepted Document Types
/classify uses the same upload path as /extract — the same accepted formats, the same size limits, and both direct file uploads and file_url.
| Category | Extensions |
|---|---|
.pdf | |
| Images | .jpg, .jpeg, .png, .webp |
| Office | .docx, .pptx, .xlsx, .xlsm, .xls, .xlsb |
| Data | .csv |
| Web | .html, .htm |
.webp, .csv, .xlsb) are converted on upload before classification, exactly as they are for /extract.
page_range, while Office and HTML files are billed for every page. See Credits.Async Mode
Setasync: true to return immediately with a job ID for polling. See Polling for Results. On completion, the job’s result carries the same body as the sync response.
/classify is currently REST-only: it is not yet exposed as a Python/TypeScript SDK method, a CLI command, or an MCP tool. Call it over HTTP as shown below.Request
Provide eitherfile (multipart upload) or file_url (JSON body) — not both.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
file | file | XOR | The document to classify (multipart/form-data upload). |
file_url | string | XOR | URL of the document to classify (application/json body). |
classify_config | object | Yes | Candidate classifications (JSON string when sent as a multipart form field). |
page_range | string | No | Pages to inspect, e.g. "1-5" or "1,3,5-7". Default: first 5 pages, clamped to the document length. |
async | boolean | No | If true, returns immediately with a job_id for polling. Default: false. |
parent_job_id | uuid | No | Optional parent job ID for tracking. |
Classify Config (classify_config)
{
"classifications": {
"<name>": {
"description": "string, required — what documents belong to this class",
"pipeline_id": "string, optional — pipeline to route matches to"
}
// ... one entry per candidate classification
}
}
<name>is any label you choose; Pulse returns exactly one of these names.descriptiontells Pulse what belongs in each class — make it specific and mutually distinct.pipeline_idis optional routing metadata. When provided, it must reference a pipeline your organization owns (otherwise the request is rejected withPIPELINE_003). The matched classification’spipeline_idis echoed back so you can route the document next.
Always include a catch-all classification
/classify always returns one of the names you supplied — there is no built-in “none of the above”, no null result, and no confidence score. A document that matches none of your classifications is not rejected: it is assigned to whichever class comes closest, with nothing in the response to flag it as a poor match.pipeline_id.
{
"classifications": {
"bank_statement": {
"description": "Bank or account statements: balances, transaction lists, deposits and withdrawals.",
"pipeline_id": "b1a2c3d4-..."
},
"invoice": {
"description": "Invoices or bills requesting payment: line items, amounts due, payment terms.",
"pipeline_id": "e5f6a7b8-..."
},
"unrecognized": {
// No pipeline_id — so classify_output.pipeline_id comes back null
"description": "Any document that does not clearly match one of the other classifications — including unrelated document types, blank or illegible scans, and standalone cover pages."
}
}
}
null classify_output.pipeline_id then becomes your “don’t extract this” signal:
{
"classification": "unrecognized",
"classify_output": {
"classification": "unrecognized",
"pipeline_id": null,
"page_range": "1-5"
}
}
PROC_001 error in the case where Pulse answers off-list, since any name outside your classifications keys is rejected.
Response
Synchronous Response (200)
| Field | Type | Description |
|---|---|---|
classification | string | The winning classification name — always one of your classifications keys (never a “no match” value) |
classify_output.classification | string | Same as classification |
classify_output.pipeline_id | uuid | null | The matched classification’s pipeline_id, or null if it had none — see catch-all classifications |
classify_output.page_range | string | The effective page range used for classification (after defaulting/clamping) |
job_id | uuid | Identifier of the request |
credits_used | number | Credits deducted for this call |
Async Response (202)
| Field | Type | Description |
|---|---|---|
job_id | string | Job ID for polling |
status | string | "pending" |
message | string | Human-readable description |
Example Usage
curl -X POST https://api.runpulse.com/classify \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@document.pdf" \
-F 'classify_config={
"classifications": {
"bank_statement": {
"description": "Bank or account statements: balances, transaction lists, deposits and withdrawals.",
"pipeline_id": "b1a2c3d4-..."
},
"invoice": {
"description": "Invoices or bills requesting payment: line items, amounts due, payment terms.",
"pipeline_id": "e5f6a7b8-..."
},
"unrecognized": {
"description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans."
}
}
}'
curl -X POST https://api.runpulse.com/classify \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_url": "https://example.com/document.pdf",
"page_range": "1-5",
"classify_config": {
"classifications": {
"bank_statement": {
"description": "Bank or account statements: balances, transaction lists.",
"pipeline_id": "b1a2c3d4-..."
},
"invoice": {
"description": "Invoices or bills requesting payment.",
"pipeline_id": "e5f6a7b8-..."
},
"unrecognized": {
"description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans."
}
}
}
}'
import json
import requests
resp = requests.post(
"https://api.runpulse.com/classify",
headers={"x-api-key": "YOUR_API_KEY"},
files={"file": open("document.pdf", "rb")},
data={
"classify_config": json.dumps({
"classifications": {
"bank_statement": {
"description": "Bank or account statements: balances, transaction lists.",
"pipeline_id": "b1a2c3d4-...",
},
"invoice": {
"description": "Invoices or bills requesting payment.",
"pipeline_id": "e5f6a7b8-...",
},
"unrecognized": {
"description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans.",
},
}
})
},
)
result = resp.json()
print(result["classification"], result["classify_output"]["pipeline_id"])
Example Response
{
"classification": "bank_statement",
"classify_output": {
"classification": "bank_statement",
"pipeline_id": "b1a2c3d4-...",
"page_range": "1-5"
},
"job_id": "e3b0c442-...",
"credits_used": 2.5
}
classification is returned at the top level for quick access and repeated inside classify_output, which keeps the routing decision (classification, pipeline_id, page_range) self-contained for pipeline consumers. Both always hold the same value.Routing to a Pipeline
/classify only tells you which pipeline to use — it does not execute it. Take the returned pipeline_id and run that pipeline with the same document; its own extraction settings then apply.
classify can also be used as the first step of an ad-hoc pipeline, where it classifies the raw document before the downstream steps run. It cannot be combined with batch_extract (classify needs a single document), and it is currently supported with inline config only.
Credits
/classify is billed at 0.5 credits per page — half the /extract rate (1 credit/page).
- PDFs / images: billed for the pages in the (defaulted/clamped)
page_range. - Office / HTML: billed for every page, even when
page_rangeis smaller.
Error Responses
| Status | Error | Description |
|---|---|---|
| 400 | REQ_004 | Missing classify_config or empty classifications |
| 400 | REQ_002 | classify_config is not valid JSON |
| 400 | REQ_006 | Invalid or out-of-range page_range |
| 400 | PIPELINE_003 | A classification’s pipeline_id doesn’t exist for your organization |
| 400 | FILE_* | Unsupported file type, file too large, or bad/blocked URL (same as /extract) |
| 401 | Unauthorized | Invalid or missing API key |
| 429 | Rate limit exceeded | Too many requests |
| 500 | PROC_001 | Processing failed — including the case where Pulse returns a name that isn’t one of your classifications keys (add a catch-all) |
Best Practices
Include a catch-all classification
Include a catch-all classification
pipeline_id gives unexpected documents somewhere to land and gives you a null pipeline_id to branch on. See Always include a catch-all classification.Write specific, mutually distinct descriptions
Write specific, mutually distinct descriptions
Keep the default page range unless you have a reason not to
Keep the default page range unless you have a reason not to
page_range has no hard cap, but large ranges scale credits linearly.Treat the result as routing guidance
Treat the result as routing guidance
/classify is persisted — the routed pipeline’s /extract produces the durable output.Authorizations
Body
Request body for classifying a document. Provide exactly one of
file (multipart upload) or file_url (JSON body). In
multipart/form-data requests, classify_config is sent as a JSON
string form field.
Candidate classifications the document is matched against.
Show child attributes
Show child attributes
Document to upload directly (multipart/form-data only). Required unless file_url is provided.
Public or pre-signed URL that Pulse will download and classify. Required unless file is provided.
Pages to inspect, e.g. "1-5" or "1,3,5-7". Defaults to the first 5 pages, clamped to the document length. For Office/HTML formats, every page is billed.
If true, returns immediately with a job_id for polling via GET /job/{jobId}. Otherwise processes synchronously.
Optional parent job id for tracking.
Response
Classification result (when async=false or omitted), or the single-use download stub when force_url was set.
- Option 1
- Option 2
Result of document classification.
The winning classification name — one of the keys from the request's classifications object.
Details of the winning classification.
Show child attributes
Show child attributes
How certain the classification is, from 0 to 1. Present when the document was classified from its text; absent otherwise.
0 <= x <= 1Identifier of the request for tracking and support.
Number of credits consumed by this request.