Validated workflow API

Validate the workflow before API use

Start in the workstation, confirm the reviewed output matches your workflow, then use the API for repeatable extraction runs.

Base: https://pdf2text.ai/api/v1

Authentication

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}`
};

Usage Limits

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 plans
POST

Upload Documents

Upload 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.

Endpoint

POST /api/v1/documents/upload/

Parameters

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();
201 Created
{
  "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.

POST

Start Extraction Run

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.

Endpoint

POST /api/v1/groups/{group_id}/runs/

Request Body

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();
202 Accepted
{
  "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
  }
}
GET

Fetch Results

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.

Endpoints

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

Response Status Codes

200 401 404
# 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);
Response with include=results
{
  "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 */ }
      }
    ]
  }
}
GET

Templates

List available extraction templates. Use template IDs to force specific extraction schemas.

Endpoint

GET /api/v1/templates/
Manage 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}`));
Response
{
  "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"
    }
  ]
}

Need Help?

Use the Review Queue to inspect extracted fields and keep review context.