API Documentation

Integrate PDFDrill into your applications

API-key endpoints

Use the full public paths below. For the statement examples, set PDFDRILL_BASE_URL=https://pdfdrill.com. The general PDF conversion base URL includes both API prefixes.

  • POST /api/api/upload
  • GET /api/api/jobs/{job_id}
  • GET /api/api/jobs/{job_id}/download
  • POST /api/v1/ledgers
  • GET /api/v1/ledgers/{batch_id}
  • GET /api/v1/ledgers/{batch_id}/export

Getting Started

The PDFDrill API allows you to convert PDFs programmatically. API access is available on Business and manually enabled Enterprise accounts. All requests require authentication via API key.

Base URL

https://pdfdrill.com/api/api

Authentication

Send your API key in the Authorization header using the Bearer scheme:

Authorization: Bearer YOUR_API_KEY

Generate an API key in your dashboard

Statements to Ledger

Ledger API

Business includes API access. Send multiple statement PDFs in one batch, poll the batch status, then download a ledger export when processing is complete. API jobs use your plan's page quota and export entitlements. API keys are sent as a Bearer token.

AuthenticationAuthorization: Bearer YOUR_BUSINESS_API_KEYKeep keys private and never put them in browser code or source control.

POST /api/v1/ledgers

Create a named batch with multipart form fields name and one or more files[] PDF parts.

curl -X POST "$PDFDRILL_BASE_URL/api/v1/ledgers" \
  -H "Authorization: Bearer $PDFDRILL_API_KEY" \
  -F "name=Client year-end" \
  -F "files[]=@statement-january.pdf" \
  -F "files[]=@statement-february.pdf"

The response includes a batchId to use for polling.

GET /api/v1/ledgers/{batchId}

Poll for the batch status, statement results, balance checks and flagged row summary.

curl "$PDFDRILL_BASE_URL/api/v1/ledgers/$BATCH_ID" \
  -H "Authorization: Bearer $PDFDRILL_API_KEY"

GET /api/v1/ledgers/{batchId}/export

Download an export with format set to excel, csv, xero, quickbooks3, quickbooks4, qbo, or pdf. For accounting import formats, provide one account_key; each export contains one account. QuickBooks Web Connect (.QBO) is limited to importing transactions into QuickBooks Online's For review list. It returns up to 1,000 rows per file; larger exports are split into a ZIP. Re-uploading the same file doesn't create duplicates. If any statements fail checks, include confirm=true to acknowledge exporting anyway.

curl "$PDFDRILL_BASE_URL/api/v1/ledgers/$BATCH_ID/export?format=excel&confirm=true" \
  -H "Authorization: Bearer $PDFDRILL_API_KEY" \
  --output ledger.xlsx

curl "$PDFDRILL_BASE_URL/api/v1/ledgers/$BATCH_ID/export?format=xero&account_key=ACCOUNT_KEY" \
  -H "Authorization: Bearer $PDFDRILL_API_KEY" \
  --output xero-import.csv

curl "$PDFDRILL_BASE_URL/api/v1/ledgers/$BATCH_ID/export?format=qbo&account_key=ACCOUNT_KEY" \
  -H "Authorization: Bearer $PDFDRILL_API_KEY" \
  --output quickbooks-web-connect.qbo

Quotas and access

  • Business API requests and successfully extracted pages are subject to the Business plan quota and configured rate limit. API access is also available on manually enabled Enterprise accounts.
  • Pages from failed extraction do not consume page quota; successfully extracted pages count even when a reconciliation check fails.
  • Export formats are governed by the plan configuration. Check your account plan for available formats.

Business and Enterprise rate limit: 100 requests per minute.

Endpoints

POST/api/api/upload

Upload a PDF for processing

Request

curl -X POST https://pdfdrill.com/api/api/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@document.pdf"

Example response (abbreviated)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued",
  "created_at": "2025-11-24T12:00:00Z"
}

GET/api/api/jobs/:id

Check job status. Once it is done, download a result with GET /api/api/jobs/:id/download?output_type=excel using the same Bearer header. Storage references in result_paths are not public download links.

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "done",
  "num_pages": 2,
  "result_paths": {
    "excel": "<private storage reference>"
  }
}

Code Examples

Python

import os
import requests

API_KEY = os.environ["PDFDRILL_API_KEY"]
BASE_URL = "https://pdfdrill.com/api/api"

# Upload PDF
with open("document.pdf", "rb") as f:
    response = requests.post(
        f"{BASE_URL}/upload",
        headers={"Authorization": f"Bearer {API_KEY}"},
        files={"file": f}
    )
    response.raise_for_status()
    job_id = response.json()["id"]

# Check status (poll until done or error before downloading)
status = requests.get(
    f"{BASE_URL}/jobs/{job_id}",
    headers={"Authorization": f"Bearer {API_KEY}"}
).json()

# Download results
if status["status"] == "done":
    response = requests.get(
        f"{BASE_URL}/jobs/{job_id}/download",
        params={"output_type": "excel"},
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    response.raise_for_status()
    with open("output.xlsx", "wb") as f:
        f.write(response.content)

JavaScript (server-side Node.js)

import { readFile, writeFile } from "node:fs/promises";

const API_KEY = process.env.PDFDRILL_API_KEY;
if (!API_KEY) throw new Error("Set PDFDRILL_API_KEY on your server");
const BASE_URL = "https://pdfdrill.com/api/api";

// Upload PDF
const formData = new FormData();
formData.append("file", new Blob([await readFile("document.pdf")], {
  type: "application/pdf"
}), "document.pdf");

const uploadResponse = await fetch(`${BASE_URL}/upload`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}` },
  body: formData
});
if (!uploadResponse.ok) throw new Error("Upload failed");
const { id: job_id } = await uploadResponse.json();

// Check status (poll until done or error before downloading)
const statusResponse = await fetch(`${BASE_URL}/jobs/${job_id}`, {
  headers: { Authorization: `Bearer ${API_KEY}` }
});
const status = await statusResponse.json();

// Download results
if (status.status === "done") {
  const result = await fetch(
    `${BASE_URL}/jobs/${job_id}/download?output_type=excel`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );
  if (!result.ok) throw new Error("Download failed");
  await writeFile("output.xlsx", Buffer.from(await result.arrayBuffer()));
}

Job webhooks

Business and manually API-enabled Enterprise accounts can optionally add webhook_url to the multipart fields sent to POST /api/api/upload. PDFDrill sends a signed JSON callback when the job finishes. The URL must be HTTPS on port 443 and resolve only to public IP addresses; redirects are not followed. Callback delivery has a bounded timeout and up to three retries. Callback delivery problems do not change job status or page quota.

curl -X POST https://pdfdrill.com/api/api/upload \
  -H "Authorization: Bearer your_api_key_here" \
  -F "file=@document.pdf" \
  -F "webhook_url=https://example.com/pdfdrill-callback" \
  -F "webhook_secret=your-private-secret-of-at-least-32-characters"

webhook_secret is optional. If supplied, it must be at least 32 characters; treat it as a secret and do not put it in source control or logs. If omitted, PDFDrill derives a signing key from the API key used for this upload. The callback body contains only event (job.done or job.error), job_id, status, pages, and error; it never contains converted file contents.

Verify the signature

PDFDrill sends X-PDFDrill-Timestamp (Unix seconds) and X-PDFDrill-Signature (sha256=<hex HMAC>). Sign the exact raw request body, not re-serialized JSON. The signed bytes are the ASCII timestamp, a period, then the raw body. Reject timestamps more than five minutes from your clock and compare signatures in constant time.

import hashlib, hmac, os, time

webhook_secret = os.getenv("PDFDRILL_WEBHOOK_SECRET")
if webhook_secret:
    key = webhook_secret.encode()
else:
    api_key = os.environ["PDFDRILL_API_KEY"]
    key = hmac.new(api_key.encode(), b"PDFDrill webhook signing key v1",
                   hashlib.sha256).digest()
timestamp = request.headers["X-PDFDrill-Timestamp"]
signature = request.headers["X-PDFDrill-Signature"]
expected = "sha256=" + hmac.new(
    key, timestamp.encode("ascii") + b"." + request.get_data(),
    hashlib.sha256
).hexdigest()
if abs(time.time() - int(timestamp)) > 300:
    reject_request()
if not hmac.compare_digest(expected, signature):
    reject_request()

Rate Limits & Pricing

PlanPricePages/MonthRate LimitOverage
Business$99/month2,000100 req/min$0.04/page beyond quota
Enterprise (manual API enablement)CustomCustom100 req/minCustom

API requests use the same page quota as web conversions. API access is not included with Free, Starter, or Pro plans.

Ready to integrate?

Create your API key and start building