Start in the workstation, confirm the reviewed output matches your workflow, then use the API for repeatable extraction runs.
https://pdf2text.ai/api/v1
Base: https://pdf2text.ai/api/v1
All API requests require a Bearer token in the Authorization header. Generate keys from account settings after you have validated a repeatable workflow.
Manage API keys# Set your API key
export P2T_API_KEY="your_api_key_here"
# Include in all requests
curl -H "Authorization: Bearer $P2T_API_KEY" ...
import requests
import os
API_KEY = os.environ["P2T_API_KEY"]
headers = {"Authorization": f"Bearer {API_KEY}"}
const API_KEY = process.env.P2T_API_KEY;
const headers = {
'Authorization': `Bearer ${API_KEY}`
};
Firm includes 1500 pages per month for reviewed workstation workflows and validated API extraction runs. Each processed page uses one page from that allowance.
Usage is measured by processed pages, not API calls. Uploading files and downloading results or exports does not use pages, but running extraction again processes and uses those pages again.
Supported uploads are PDF, PNG, JPEG, and WebP, up to 50 MiB per file. The current deployment also limits the complete multipart request to 100 MiB, including all files and form overhead. Uploads are limited to 20 files per request, 100 files per group, 500 pages per document or run, and 1,000 stored pages per group. Images may be up to 20,000 pixels on either side and 40 megapixels; PDF pages that would exceed those raster bounds at the OCR render resolution are rejected.
There is no separate API-call quota. Extraction runs are asynchronous and enter the processing queue, so completion time can vary with document size and system load.
Fair-use request throttles default to 30 uploads, 60 run starts, and 600 reads or exports per account per hour. A 429 response includes Retry-After; wait for that interval before retrying.
Use the workstation to validate document layouts, review rules, and export formats before relying on API automation.
Compare plansUpload one or more PDF, PNG, JPEG, or WebP files to create a document group. File bytes, page counts, and image dimensions are verified before anything is saved. A successful upload returns 201 Created with the verified page_count for every document.
POST /api/v1/documents/upload/
file / files
binary
PDF, PNG, JPEG, or WebP (required; repeat files for multiple uploads)
grouping_id
uuid
Optional. Add to one of your existing API groups. Anonymous workspaces cannot be claimed by UUID, and an active run makes its group immutable until completion.
document_type
string
Optional. Classification hint.
# Upload a PDF file
curl -X POST https://pdf2text.ai/api/v1/documents/upload/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-F "file=@invoice.pdf"
# Add to existing group
curl -X POST https://pdf2text.ai/api/v1/documents/upload/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-F "file=@invoice2.pdf" \
-F "grouping_id=a1b2c3d4-..."
import requests
url = "https://pdf2text.ai/api/v1/documents/upload/"
# Upload single file
with open("invoice.pdf", "rb") as f:
response = requests.post(
url,
headers=headers,
files={"file": f}
)
data = response.json()
group_id = data["group_id"]
const { readFile } = require('node:fs/promises');
const form = new FormData();
form.append(
'file',
new Blob([await readFile('invoice.pdf')], { type: 'application/pdf' }),
'invoice.pdf'
);
const response = await fetch(
'https://pdf2text.ai/api/v1/documents/upload/',
{
method: 'POST',
headers,
body: form
}
);
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
const { group_id } = await response.json();
{
"status": "success",
"group_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"documents": [
{
"doc_id": 42,
"uid": "1847f600-...",
"grouping_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"original_filename": "invoice.pdf",
"file_type": "pdf",
"page_count": 3,
"status": "pending"
}
]
}
Append files to a group only before its first run. For later invoices, create a new group; otherwise an unfiltered run processes every document already in that group.
Kick off asynchronous processing for a document group. Use auto-routing when PDF2TEXT should classify documents before extraction.
Send an Idempotency-Key header on every start request. Repeating the same key and body returns the same run; reusing the key for a different request returns 409. After a terminal failure, use a new key to intentionally create a replacement run. A newly queued run returns 202 Accepted. Only one run per group may be active at a time, and active runs reserve the account pages they can process.
A 202 response confirms that the run was accepted, not that every requested page is funded. If the shared account balance is smaller than the run, the API processes the reserved pages and finishes as partial with credit_limit_reached=true. Check the terminal run status before treating the import as complete.
Callback URLs must use public HTTPS and cannot redirect to another destination. Delivery is asynchronous, has a 10-second request timeout, and makes up to four retries for transient connection errors, HTTP 429, and HTTP 5xx responses using 30-second, 2-minute, and 10-minute backoff intervals.
When callback_secret is supplied, verify X-PDF2TEXT-Signature by computing HMAC-SHA256 over the exact request body and comparing it with the sha256= value. Use X-PDF2TEXT-Run-ID or X-PDF2TEXT-Idempotency-Key to deduplicate delivery.
The callback JSON body is {"event": "idp.run.completed", "run": {...}}; run uses the same full result shape as GET ?include=results. A signature header is sent only when callback_secret was included in the start request.
POST /api/v1/groups/{group_id}/runs/
auto_route
boolean
Auto-classify (default: true)
extraction_mode
enum
auto, raw_text, financial_table, general_table, or unified. Use template_id or template_slug for a custom schema.
template_id / template_slug
uuid / string
Use exactly one to force a specific template
selected_pages
array
Optional non-empty list of {docUid, pageNumber}; invalid pages reject the whole request
construction_project_id
uuid
Optional project owned by the API account
callback_url
string
Webhook URL
callback_secret
string
HMAC signing secret
# Start extraction with auto-routing
curl -X POST https://pdf2text.ai/api/v1/groups/$GROUP_ID/runs/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-H "Idempotency-Key: invoice-import-123" \
-H "Content-Type: application/json" \
-d '{"auto_route": true}'
# Start extraction run
url = f"https://pdf2text.ai/api/v1/groups/{group_id}/runs/"
response = requests.post(
url,
headers={**headers, "Idempotency-Key": "invoice-import-123"},
json={"auto_route": True}
)
run_data = response.json()
run_id = run_data["run_id"]
// Start extraction run
const response = await fetch(
`https://pdf2text.ai/api/v1/groups/${groupId}/runs/`,
{
method: 'POST',
headers: {
...headers,
'Idempotency-Key': 'invoice-import-123',
'Content-Type': 'application/json'
},
body: JSON.stringify({ auto_route: true })
}
);
const { run_id } = await response.json();
{
"status": "success",
"run_id": "b3143b91-...",
"job_id": "idp-run-b3143b91-...",
"group_name": "ocr_job_a1b2c3d4...",
"idempotent_replay": false,
"run": {
"id": "b3143b91-...",
"status": "queued",
"job_id": "idp-run-b3143b91-...",
"total_pages": 3,
"pages_processed": 0
}
}
Poll the run endpoint or use webhooks to retrieve extraction results. Export as JSON or Excel.
The default run response is lightweight for frequent polling and is never cached. Read run.status inside the nested run object. Terminal statuses are complete, partial, and error. After a terminal status, request include=results once to retrieve full_text, structured_output, and documents.
credits_remaining is retained for compatibility and records the historical balance after that run. credits_remaining_after_run names the same historical value explicitly, while account_credits_remaining reports the account's current shared balance when the response is generated.
GET /api/v1/runs/{run_id}/
GET /api/v1/runs/{run_id}/?include=results
GET /api/v1/runs/{run_id}/export.json
GET /api/v1/runs/{run_id}/export.xlsx
# Lightweight status poll
curl https://pdf2text.ai/api/v1/runs/$RUN_ID/ \
-H "Authorization: Bearer $P2T_API_KEY"
# Fetch completed results once, then export
curl "https://pdf2text.ai/api/v1/runs/$RUN_ID/?include=results" \
-H "Authorization: Bearer $P2T_API_KEY"
curl -o results.xlsx \
https://pdf2text.ai/api/v1/runs/$RUN_ID/export.xlsx \
-H "Authorization: Bearer $P2T_API_KEY"
import time
run_url = f"https://pdf2text.ai/api/v1/runs/{run_id}/"
terminal = {"complete", "partial", "error"}
delay = 2
# Poll lightweight state with bounded exponential backoff
while True:
response = requests.get(run_url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", delay))
time.sleep(retry_after)
delay = min(30, max(10, delay * 2))
continue
response.raise_for_status()
run = response.json()["run"]
if run["status"] in terminal:
break
time.sleep(delay)
delay = min(30, max(10, delay * 2))
if run["status"] == "error":
raise RuntimeError(run["error_message"])
results = requests.get(run_url, params={"include": "results"}, headers=headers).json()["run"]
excel = requests.get(
f"{run_url}export.xlsx",
headers=headers
)
with open("results.xlsx", "wb") as f:
f.write(excel.content)
// Poll lightweight state
const pollResults = async (runId) => {
const runUrl = `https://pdf2text.ai/api/v1/runs/${runId}/`;
const terminal = new Set(['complete', 'partial', 'error']);
let delay = 2000;
while (true) {
const response = await fetch(runUrl, { headers });
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || delay / 1000);
await new Promise(r => setTimeout(r, retryAfter * 1000));
delay = Math.min(30000, Math.max(10000, delay * 2));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { run } = await response.json();
if (terminal.has(run.status)) {
if (run.status === 'error') throw new Error(run.error_message);
const resultResponse = await fetch(`${runUrl}?include=results`, { headers });
return (await resultResponse.json()).run;
}
await new Promise(r => setTimeout(r, delay));
delay = Math.min(30000, Math.max(10000, delay * 2));
}
};
const results = await pollResults(runId);
{
"status": "success",
"run": {
"id": "b3143b91-...",
"status": "complete",
"total_pages": 3,
"pages_processed": 3,
"credits_remaining_after_run": 497,
"account_credits_remaining": 497,
"full_text": "...",
"structured_output": { /* grouped output */ },
"documents": [
{
"doc_id": 42,
"document_name": "invoice.pdf",
"ocr_text": "...",
"structured_output": { /* document output */ }
}
]
}
}
List available extraction templates. Use template IDs to force specific extraction schemas.
GET /api/v1/templates/
# List available templates
curl https://pdf2text.ai/api/v1/templates/ \
-H "Authorization: Bearer $P2T_API_KEY"
# List available templates
response = requests.get(
"https://pdf2text.ai/api/v1/templates/",
headers=headers
)
templates = response.json()["templates"]
for t in templates:
print(f"{t['name']}: {t['id']}")
// List available templates
const response = await fetch(
'https://pdf2text.ai/api/v1/templates/',
{ headers }
);
const { templates } = await response.json();
templates.forEach(t => console.log(`${t.name}: ${t.id}`));
{
"status": "success",
"templates": [
{
"id": "198aa5be-ea8b-4d67-bdb5-625f5d7d29e8",
"slug": "invoice",
"name": "Invoice",
"description": "Standard invoice extraction",
"extraction_mode": "financial_table"
},
{
"id": "4317707a-f0da-4c2f-a1ec-d0119d96b55c",
"slug": "receipt",
"name": "Receipt",
"description": "Receipt and expense extraction",
"extraction_mode": "financial_table"
}
]
}
Use the Review Queue to inspect extracted fields and keep review context.
# Set your API key
export P2T_API_KEY="your_api_key_here"
# Include in all requests
curl -H "Authorization: Bearer $P2T_API_KEY" ...
import requests
import os
API_KEY = os.environ["P2T_API_KEY"]
headers = {"Authorization": f"Bearer {API_KEY}"}
const API_KEY = process.env.P2T_API_KEY;
const headers = {
'Authorization': `Bearer ${API_KEY}`
};
# Upload a PDF file
curl -X POST https://pdf2text.ai/api/v1/documents/upload/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-F "file=@invoice.pdf"
# Add to existing group
curl -X POST https://pdf2text.ai/api/v1/documents/upload/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-F "file=@invoice2.pdf" \
-F "grouping_id=a1b2c3d4-..."
import requests
url = "https://pdf2text.ai/api/v1/documents/upload/"
# Upload single file
with open("invoice.pdf", "rb") as f:
response = requests.post(
url,
headers=headers,
files={"file": f}
)
data = response.json()
group_id = data["group_id"]
const { readFile } = require('node:fs/promises');
const form = new FormData();
form.append(
'file',
new Blob([await readFile('invoice.pdf')], { type: 'application/pdf' }),
'invoice.pdf'
);
const response = await fetch(
'https://pdf2text.ai/api/v1/documents/upload/',
{
method: 'POST',
headers,
body: form
}
);
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
const { group_id } = await response.json();
{
"status": "success",
"group_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"documents": [
{
"doc_id": 42,
"uid": "1847f600-...",
"grouping_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"original_filename": "invoice.pdf",
"file_type": "pdf",
"page_count": 3,
"status": "pending"
}
]
}
Append files to a group only before its first run. For later invoices, create a new group; otherwise an unfiltered run processes every document already in that group.
# Start extraction with auto-routing
curl -X POST https://pdf2text.ai/api/v1/groups/$GROUP_ID/runs/ \
-H "Authorization: Bearer $P2T_API_KEY" \
-H "Idempotency-Key: invoice-import-123" \
-H "Content-Type: application/json" \
-d '{"auto_route": true}'
# Start extraction run
url = f"https://pdf2text.ai/api/v1/groups/{group_id}/runs/"
response = requests.post(
url,
headers={**headers, "Idempotency-Key": "invoice-import-123"},
json={"auto_route": True}
)
run_data = response.json()
run_id = run_data["run_id"]
// Start extraction run
const response = await fetch(
`https://pdf2text.ai/api/v1/groups/${groupId}/runs/`,
{
method: 'POST',
headers: {
...headers,
'Idempotency-Key': 'invoice-import-123',
'Content-Type': 'application/json'
},
body: JSON.stringify({ auto_route: true })
}
);
const { run_id } = await response.json();
{
"status": "success",
"run_id": "b3143b91-...",
"job_id": "idp-run-b3143b91-...",
"group_name": "ocr_job_a1b2c3d4...",
"idempotent_replay": false,
"run": {
"id": "b3143b91-...",
"status": "queued",
"job_id": "idp-run-b3143b91-...",
"total_pages": 3,
"pages_processed": 0
}
}
# Lightweight status poll
curl https://pdf2text.ai/api/v1/runs/$RUN_ID/ \
-H "Authorization: Bearer $P2T_API_KEY"
# Fetch completed results once, then export
curl "https://pdf2text.ai/api/v1/runs/$RUN_ID/?include=results" \
-H "Authorization: Bearer $P2T_API_KEY"
curl -o results.xlsx \
https://pdf2text.ai/api/v1/runs/$RUN_ID/export.xlsx \
-H "Authorization: Bearer $P2T_API_KEY"
import time
run_url = f"https://pdf2text.ai/api/v1/runs/{run_id}/"
terminal = {"complete", "partial", "error"}
delay = 2
# Poll lightweight state with bounded exponential backoff
while True:
response = requests.get(run_url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", delay))
time.sleep(retry_after)
delay = min(30, max(10, delay * 2))
continue
response.raise_for_status()
run = response.json()["run"]
if run["status"] in terminal:
break
time.sleep(delay)
delay = min(30, max(10, delay * 2))
if run["status"] == "error":
raise RuntimeError(run["error_message"])
results = requests.get(run_url, params={"include": "results"}, headers=headers).json()["run"]
excel = requests.get(
f"{run_url}export.xlsx",
headers=headers
)
with open("results.xlsx", "wb") as f:
f.write(excel.content)
// Poll lightweight state
const pollResults = async (runId) => {
const runUrl = `https://pdf2text.ai/api/v1/runs/${runId}/`;
const terminal = new Set(['complete', 'partial', 'error']);
let delay = 2000;
while (true) {
const response = await fetch(runUrl, { headers });
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || delay / 1000);
await new Promise(r => setTimeout(r, retryAfter * 1000));
delay = Math.min(30000, Math.max(10000, delay * 2));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { run } = await response.json();
if (terminal.has(run.status)) {
if (run.status === 'error') throw new Error(run.error_message);
const resultResponse = await fetch(`${runUrl}?include=results`, { headers });
return (await resultResponse.json()).run;
}
await new Promise(r => setTimeout(r, delay));
delay = Math.min(30000, Math.max(10000, delay * 2));
}
};
const results = await pollResults(runId);
{
"status": "success",
"run": {
"id": "b3143b91-...",
"status": "complete",
"total_pages": 3,
"pages_processed": 3,
"credits_remaining_after_run": 497,
"account_credits_remaining": 497,
"full_text": "...",
"structured_output": { /* grouped output */ },
"documents": [
{
"doc_id": 42,
"document_name": "invoice.pdf",
"ocr_text": "...",
"structured_output": { /* document output */ }
}
]
}
}
# List available templates
curl https://pdf2text.ai/api/v1/templates/ \
-H "Authorization: Bearer $P2T_API_KEY"
# List available templates
response = requests.get(
"https://pdf2text.ai/api/v1/templates/",
headers=headers
)
templates = response.json()["templates"]
for t in templates:
print(f"{t['name']}: {t['id']}")
// List available templates
const response = await fetch(
'https://pdf2text.ai/api/v1/templates/',
{ headers }
);
const { templates } = await response.json();
templates.forEach(t => console.log(`${t.name}: ${t.id}`));
{
"status": "success",
"templates": [
{
"id": "198aa5be-ea8b-4d67-bdb5-625f5d7d29e8",
"slug": "invoice",
"name": "Invoice",
"description": "Standard invoice extraction",
"extraction_mode": "financial_table"
},
{
"id": "4317707a-f0da-4c2f-a1ec-d0119d96b55c",
"slug": "receipt",
"name": "Receipt",
"description": "Receipt and expense extraction",
"extraction_mode": "financial_table"
}
]
}