curl --request POST \
--url https://api.runpulse.com/form/detect \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <api-key>' \
--form file='@example-file' \
--form 'file_url=<string>' \
--form form_id=3c90c3cc-0d44-4b50-8888-8dd25736052a \
--form 'page_range=<string>' \
--form 'async=<string>'import requests
url = "https://api.runpulse.com/form/detect"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"file_url": "<string>",
"form_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_range": "<string>",
"async": "<string>"
}
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('file', '<string>');
form.append('file_url', '<string>');
form.append('form_id', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
form.append('page_range', '<string>');
form.append('async', '<string>');
const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
options.body = form;
fetch('https://api.runpulse.com/form/detect', 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/form/detect",
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=\"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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\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/form/detect"
payload := strings.NewReader("-----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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\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/form/detect")
.header("x-api-key", "<api-key>")
.body("-----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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/form/detect")
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=\"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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"form_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_count": 2,
"pdf_url": "<string>",
"form_fields": [
{
"page_number": 2,
"bounding_box": [
0.5
],
"text": "<string>",
"type": "text",
"row": 1,
"col": 1,
"table_idx": 1,
"checkbox_details": [
{
"center_coord": [
0.5
],
"selected": true,
"text": "<string>"
}
]
}
],
"fields_filled": 1,
"fields_cleared": 1,
"credits_used": 123,
"plan_info": {
"tier": "<string>",
"total_credits_used": 123,
"pages_used": 1,
"note": "<string>"
}
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>",
"queuedAt": "2023-11-07T05:31:56Z",
"credits_used": 123
}Detect Form Fields
Run cell detection on a PDF and return the detected form_fields
along with a reusable form_id. No LLM matching, no fill, no
clear — this is the OCR / layout step that /form/fill and
/form/clear would otherwise run internally.
The returned form_id references the uploaded PDF and its
detected layout, and can be passed back to a subsequent
/form/fill, /form/clear, or /form/detect call as the
single input source — Pulse will skip detection on the fast
path and reuse the cached cells.
Input modes — provide exactly one of:
form_id— re-detect cells on a previously stored PDF. Useful when callers want to refresh layout after editing or when chaining detect calls.file_url— public or pre-signed URL Pulse will download.file— direct binary upload of the PDF.
All three input modes ride on the same multipart/form-data
request body. (Callers sending Content-Type: application/json
with form_id / file_url are still accepted server-side for
backward compatibility, but the SDKs only model the multipart
form.)
Optional page_range (alias pages, e.g. "1-3,5") restricts
the operation to a subset of pages.
Synchronous by default — returns the detected layout inline.
Set async: true to receive {job_id, status: "pending"}
immediately and poll GET /job/.
Billed at 1 credit per page. Requires the form_filler
feature flag to be enabled for your organization.
curl --request POST \
--url https://api.runpulse.com/form/detect \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <api-key>' \
--form file='@example-file' \
--form 'file_url=<string>' \
--form form_id=3c90c3cc-0d44-4b50-8888-8dd25736052a \
--form 'page_range=<string>' \
--form 'async=<string>'import requests
url = "https://api.runpulse.com/form/detect"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"file_url": "<string>",
"form_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_range": "<string>",
"async": "<string>"
}
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('file', '<string>');
form.append('file_url', '<string>');
form.append('form_id', '3c90c3cc-0d44-4b50-8888-8dd25736052a');
form.append('page_range', '<string>');
form.append('async', '<string>');
const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
options.body = form;
fetch('https://api.runpulse.com/form/detect', 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/form/detect",
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=\"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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\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/form/detect"
payload := strings.NewReader("-----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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\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/form/detect")
.header("x-api-key", "<api-key>")
.body("-----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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.runpulse.com/form/detect")
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=\"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=\"form_id\"\r\n\r\n3c90c3cc-0d44-4b50-8888-8dd25736052a\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\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"form_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"page_count": 2,
"pdf_url": "<string>",
"form_fields": [
{
"page_number": 2,
"bounding_box": [
0.5
],
"text": "<string>",
"type": "text",
"row": 1,
"col": 1,
"table_idx": 1,
"checkbox_details": [
{
"center_coord": [
0.5
],
"selected": true,
"text": "<string>"
}
]
}
],
"fields_filled": 1,
"fields_cleared": 1,
"credits_used": 123,
"plan_info": {
"tier": "<string>",
"total_credits_used": 123,
"pages_used": 1,
"note": "<string>"
}
}{
"job_id": "<string>",
"status": "pending",
"message": "<string>",
"queuedAt": "2023-11-07T05:31:56Z",
"credits_used": 123
}Overview
form_id. Returns a FormResult synchronously by default. Set async: true to run in the background and poll GET /job/jobId for the result./form/detect is the entry point for the form-filler workflow when you want to inspect the fields Pulse identified on a PDF before filling or clearing them. Use it to preview detected fields, fix a misclassified cell, see which checkboxes are currently selected, or cache the detection result for repeated chained calls.
The returned form_id references the uploaded PDF and its detected layout, and can be passed back to any of /form/detect, /form/fill, or /form/clear as the single input source. Pulse will reuse the cached layout instead of re-detecting it.
Providing the PDF
Provide the PDF in exactly one of the following ways:form_id: re-detect on a previously stored PDF (returned by an earlier/form/detect,/form/fill, or/form/clearcall). Useful when chaining detect calls or refreshing layout after edits.file_url: public or presigned URL to a PDF.file: PDF uploaded inline with the request.
400.
multipart/form-data request body — that’s how the SDKs send every call. JSON bodies (Content-Type: application/json) with form_id or file_url are still accepted server-side for backward compatibility, but the SDKs only model the multipart form.Pricing
Billed at 1 credit per page of the PDF being processed. Every response also returns a top-levelcredits_used for this request and a cumulative plan_info.total_credits_used snapshot for your organization.
Request
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
form_id | string (uuid) | One of these | Re-detect on a previously stored PDF. |
file_url | string (uri) | One of these | Public or presigned URL of a PDF. |
file | binary | One of these | PDF uploaded inline with the request. |
page_range | string | No | 1-based page filter, for example "1,3-5". Alias pages accepted. |
async | boolean | No | When true, returns { job_id, status: "pending" } immediately (HTTP 202) and processes the job in the background. Default false. |
Response
Sync (200): FormResult
When async is false (default), the call returns a FormResult body directly. Since /form/detect does not modify the PDF, neither fields_filled nor fields_cleared is present.
| Field | Type | Description |
|---|---|---|
form_id | string (uuid) | ID of the form record produced by this run. Pass to a subsequent /form/detect, /form/fill, or /form/clear call. |
page_count | integer | Number of pages in the PDF. |
pdf_url | string (uri) | URL to download the (unmodified) PDF binary. Always points at GET /results/jobId/pdf. Requires the same auth as the rest of the API. |
form_fields | array of FormCell | Detected cells. Each carries a normalized bounding_box, a type (text / checkbox / signature), the current text content, and for checkbox cells a checkbox_details[] array with per-box center coordinates, selection state, and labels. |
credits_used | number | Credits consumed by this request (1 × page_count). |
plan_info | object | { tier, total_credits_used, pages_used } cumulative billing snapshot for your organization (post-request). |
{
"form_id": "30fe08e1-922e-4012-9dfa-6aed0df430dc",
"page_count": 6,
"pdf_url": "https://api.runpulse.com/results/80690a27-ce39-4ad6-a1c7-70c7745238c3/pdf",
"form_fields": [
{
"page_number": 1,
"type": "text",
"bounding_box": [0.044, 0.038, 0.222, 0.052],
"text": "Name (as shown on your income tax return)"
},
{
"page_number": 1,
"type": "checkbox",
"bounding_box": [0.118, 0.226, 0.634, 0.241],
"text": "Individual/sole proprietor C corporation S corporation Partnership",
"checkbox_details": [
{ "center_coord": [0.125, 0.232], "selected": false, "text": "Individual/sole proprietor" },
{ "center_coord": [0.300, 0.232], "selected": false, "text": "C corporation" },
{ "center_coord": [0.418, 0.232], "selected": false, "text": "S corporation" },
{ "center_coord": [0.535, 0.232], "selected": false, "text": "Partnership" }
]
}
],
"credits_used": 6.0,
"plan_info": {
"tier": "pulse_ultra_2",
"total_credits_used": 1278.0,
"pages_used": 426
}
}
bounding_box, checkbox_details[].center_coord) are normalized to [0, 1] with a top-left origin. Multiply by your render width / height to convert to pixel coordinates.Async (202): FormJobAccepted
When async is true:
{
"job_id": "abc123-def456-ghi789",
"status": "pending"
}
result carries the same FormResult shape that the sync flow would have returned inline.
Status Codes
| Code | Description |
|---|---|
| 200 | Detected layout returned synchronously. |
| 202 | Async job accepted (async: true). Poll /job/{jobId} for the result. |
| 400 | Missing PDF or more than one PDF source provided. |
| 401 | Authentication failed or missing API key. |
| 404 | Referenced form_id not found (or belongs to a different org). |
| 500 | Internal server error. |
Example Usage
Detect From URL
from pulse import Pulse
client = Pulse(api_key="YOUR_API_KEY")
result = client.form.detect(
file_url="https://www.irs.gov/pub/irs-pdf/fw9.pdf",
)
print(f"form_id : {result.form_id}")
print(f"page_count : {result.page_count}")
print(f"# cells : {len(result.form_fields or [])}")
print(f"credits : {result.credits_used} (1 x {result.page_count} pages)")
for cell in (result.form_fields or [])[:3]:
print(f" [{cell.type}] {cell.bounding_box} {cell.text!r}")
import { PulseClient } from "pulse-ts-sdk";
const client = new PulseClient({ apiKey: "YOUR_API_KEY" });
const result = await client.form.detect({
file_url: "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
});
console.log(`form_id=${result.form_id}`);
console.log(`page_count=${result.page_count}`);
console.log(`# cells=${result.form_fields?.length ?? 0}`);
curl -X POST https://api.runpulse.com/form/detect \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file_url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"}'
File Upload
with open("intake-form.pdf", "rb") as f:
result = client.form.detect(file=f)
import * as fs from "fs";
const fileBuffer = fs.readFileSync("intake-form.pdf");
const blob = new Blob([fileBuffer], { type: "application/pdf" });
const result = await client.form.detect({ file: blob });
curl -X POST https://api.runpulse.com/form/detect \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@intake-form.pdf"
Detect, Edit, Then Fill
Detect the cells once, hand-edit any that were misclassified, and pass the edited cells back to/form/fill along with the cached form_id. The fill call reuses the cached layout instead of re-detecting it.
detect = client.form.detect(file_url="https://example.com/contract.pdf")
# Re-tag a cell the detector got wrong
edited = []
for cell in detect.form_fields or []:
if cell.text and cell.text.strip().lower() == "signature":
cell.type = "signature"
edited.append(cell)
fill = client.form.fill(
form_id=detect.form_id,
instructions="Sign as Jane Doe, dated 2026-05-01.",
form_fields=edited,
)
const detect = await client.form.detect({
file_url: "https://example.com/contract.pdf",
});
const edited = (detect.form_fields ?? []).map((cell) =>
cell.text?.trim().toLowerCase() === "signature"
? { ...cell, type: "signature" as const }
: cell,
);
const fill = await client.form.fill({
form_id: detect.form_id!,
instructions: "Sign as Jane Doe, dated 2026-05-01.",
form_fields: edited,
});
Re-detect On A Stored Form
Passform_id (instead of file_url / file) to refresh the layout on a PDF already stored by Pulse. Useful after a /form/clear round-trip, or to grab the latest cells if you suspect drift.
fresh = client.form.detect(form_id="00e2c454-4e6f-429b-bd74-320ad94b2153")
curl -X POST https://api.runpulse.com/form/detect \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"form_id": "00e2c454-4e6f-429b-bd74-320ad94b2153"}'
Authorizations
Body
/form/detect request body. All three input modes (file,
file_url, form_id) ride on this single multipart/form-data
schema; the server validates that exactly one is provided.
Direct binary upload of the PDF. Mutually exclusive with file_url and form_id.
Public or pre-signed URL of a PDF Pulse will download. Mutually exclusive with file and form_id.
Reference to a previously processed form. Mutually exclusive with file / file_url.
Restrict the operation to a subset of pages, e.g. "1-3,5".
Set to "true" to run asynchronously and receive {job_id, status} immediately.
Response
Detected layout returned synchronously.
Result body returned by /form/detect, /form/fill, and
/form/clear. For async jobs (async: true) the same shape is
served back under result on
GET /job/{jobId} [blocked].
ID of the form record produced by this run. Pass to a subsequent /form/detect, /form/fill, or /form/clear call as the single input source to iterate without re-uploading the PDF.
Number of pages in the output PDF.
x >= 1URL to download the resulting PDF binary. Always points at GET /results/{jobId}/pdf [blocked] for the originating job. Authenticated callers can replay this URL until the underlying artifact is garbage-collected.
Detected cells of the resulting PDF (refreshed from the filled/cleared output for fill/clear, or freshly detected for /form/detect). Use these as a starting point for further edits.
Show child attributes
Show child attributes
Number of cells that were filled by this run. Present on /form/fill responses only.
x >= 0Number of cells that were cleared by this run. Present on /form/clear responses only.
x >= 0Number of credits consumed by this request. Detect charges 1 credit per page; fill and clear charge 3 credits per page.
Billing tier and cumulative usage information for the calling org, including this form run.
Show child attributes
Show child attributes