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": {}
}' \
--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")) }
payload = {
"classify_config": "{
\"classifications\": {}
}",
"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\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\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\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\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>"
},
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credits_used": 123
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>",
"queuedAt": "2023-11-07T05:31:56Z",
"credits_used": 123
}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": {}
}' \
--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")) }
payload = {
"classify_config": "{
\"classifications\": {}
}",
"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\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\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\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\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>"
},
"job_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"credits_used": 123
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>",
"queuedAt": "2023-11-07T05:31:56Z",
"credits_used": 123
}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)
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
Identifier of the request for tracking and support.
Number of credits consumed by this request.