Automation port: API reference
Turn the port on in Preferences, Automation. The batch chapter says what it is for.
The automation port is a small HTTP/1.1 API on 127.0.0.1 (port 7431 by default), off until it is turned on in Preferences › Automation.
Rules every request meets
| Rule | Answer when broken |
|---|---|
Authorization: Bearer <token> with the token from Preferences | 401 |
No Origin, Sec-Fetch-* or Access-Control-Request-* header (browsers) | 403 |
Host exactly 127.0.0.1:<port> or localhost:<port> | 400 (missing or repeated), 421 |
| Request line and headers within 16 KB, 64 headers | 431 |
Body within 256 KB, with Content-Length (no chunked bodies) | 413, 411, 501 |
| Headers within 5 s, the body within 15 s of them | 408 |
| At most 240 requests a minute (a wrong token counts eleven times) | 429 with Retry-After |
| At most 8 connections at once | 503 |
POST bodies as JSON | 415 |
| OPTIONS and HEAD | 405 |
One request per connection: every answer says Connection: close. No answer carries CORS headers. Every answer is JSON; an error is {"error": "<a sentence>"}.
Routes
GET /v1/health
{"ok": true, "app": "AntiPDF", "version": "1.0.4", "jobs_running": 1, "jobs_waiting": 0}
POST /v1/jobs
Starts a job. The body is a job description:
{
"recipe": "Invoices in",
"inputs": ["D:\\Scans\\2026-10", "D:\\Scans\\extra.pdf"],
"output": {"folder": "D:\\Invoices\\2026-10", "suffix": " (processed)",
"exists": "number", "keep_folders": true, "log": true},
"dry_run": false,
"workers": 2,
"memory_mb": 2048
}
| Key | Meaning |
|---|---|
recipe | A saved recipe's name. Give this or steps. |
steps | The steps, as in a recipe file (below). |
name | With steps: a name for the log. |
inputs | Files and folders (absolute paths), at most 1000. A folder brings the documents in it and its folders; links and junctions in it are not followed. |
output.folder | Where the results go; left out or empty: beside each original. |
output.suffix | Added to each name; default " (processed)". |
output.exists | When a name is taken: number (default, keep both), skip, replace. An original is never replaced. |
output.keep_folders | With a folder: repeat the folders of the inputs. |
output.log | A log file beside the results (default true). |
dry_run | Say what would happen, write nothing. |
workers | Files worked on at once, 1 to 16 (default: chosen by the app). |
memory_mb | The memory the job may hold at once, 256 to 65536. |
certificate_password | For a Sign step, when the certificate has a password. Never written anywhere. |
Every path must be absolute and inside the folders allowed in Preferences, as written and as it really is (links followed), unless Any folder is on. A job may name at most 10,000 files and 20 GB in all.
Answers: 202 {"id": "1a04c9ebf9b3a442", "state": "running", "href": "/v1/jobs/1a04c9ebf9b3a442"}; 400 a job that cannot run (the sentence says why: an unknown step or option, steps in an order that cannot work); 403 a path outside the allowed folders, or no folder allowed yet; 404 an input that is not there; 503 sixteen jobs waiting already.
GET /v1/jobs
The jobs kept (the last 64 finished, and all running or waiting), newest first: {"jobs": [{"id": "...", "state": "finished"}]}.
GET /v1/jobs/{id}
{
"id": "1a04c9ebf9b3a442", "state": "finished", "recipe": "Invoices in",
"dry_run": false, "seconds": 12.4,
"counts": {"waiting": 0, "working": 0, "done": 2, "skipped": 1, "failed": 0, "stopped": 0},
"log": "D:\\Invoices\\2026-10\\AntiPDF batch 2026-10-12 09.31.07.log",
"files": [
{"input": "D:\\Scans\\2026-10\\a.pdf", "state": "done",
"message": "3 scanned pages read. 62% smaller (4.1 MB to 1.5 MB).",
"outputs": ["D:\\Invoices\\2026-10\\a (processed).pdf"]},
{"input": "D:\\Scans\\2026-10\\b.pdf", "state": "skipped",
"message": "It is protected with a password. Open it in AntiPDF and save a copy without the password, then add that copy.",
"outputs": []}
]
}
A job's state: waiting (for a place: two run at once), running, stopping, finished, stopped, or refused (with problem: it could not start, for example a Pro step without Pro). A file's state: waiting, working (with step, counted from 1), done, skipped, failed, stopped.
DELETE /v1/jobs/{id}
Stops a job after the steps in progress: 202 {"id": "...", "state": "stopping"}. A job not started yet, or finished, answers 200 with its state.
Steps
Each step is {"do": "<step>", <options>}. Unknown steps and options are refused with their names.
do | Options |
|---|---|
convert | (only as the first step) |
ocr | |
redact (Pro) | find: list of card, bank, id, email, phone, serial, name, address; countries: list of US, GB, DE, FR, ES, IT, NL, SE, CA, AU, JP, BR, IN, MX, PL, AT (left out: as chosen in Security › Redact); lists: names of your redaction lists |
compress | max_dpi (0 keeps sizes; 50 to 1200, default 150), quality (10 to 100, default 75) |
watermark (Pro) | text, place (across, top, bottom), size (small, medium, large), strength (light, medium, strong), colour (grey, red, blue), pages |
pdfa | level: "1b", "2b" (the default) or "3b" (not after watermark: its font is not embedded) |
extract | pages, such as "1-3, 7, 10-" |
rotate | angle (90, 180, 270), pages (empty: all) |
flatten | |
clean (Pro) | remove: list of metadata, scripts, attachments, thumbnails, private, comments, hidden_layers (default: the first three) |
sign (Pro) | reason, location (only as the last step) |
merge | name (the joined file's, default "Joined"); once |
preflight | profile: anywhere, print, pdfx-1a, pdfx-4, pdfa-1b, pdfa-1a, pdfa-2b, pdfa-2u, pdfa-2a, pdfa-3b, pdfa-3u, pdfa-3a, or own with name and its settings (fonts, min_dpi, max_dpi, colour (any, cmyk), transparency, trim, bleed, structure, anywhere, pdfx (0, 1, 4)); fix (the automatic fixes; not with a PDF/A profile), fail (a file with findings fails) |
bates | Number pages: prefix, suffix, digits (1 to 12, default 6), start (0 to 999999999999, default 1), place (top-left, top-centre, top-right, bottom-left, bottom-centre, bottom-right), margin_mm (0 to 100, default 10), font (sans, serif, mono), bold, size (4 to 72 points, default 10), colour (black, red, blue); once, not before redact. The numbers go on from file to file in the order of inputs (files one at a time); each file's result has "bates": [first, last, pages], and a CSV of the ranges is written beside the results |
split | every (pages per part) or ranges ("1-3, 4-10", a part each); once, not before merge |
text, markdown | (only as the last step) |
tables | format: xlsx (default), csv or markdown; pages (empty: all). Only as the last step; a file with no tables is skipped |
find_pages | words (separated by commas), all (every word on the page; default any), match_case, whole_words (default true); or field (a form field's name or part of it) and value (what its value holds; empty: filled in). A file with no page that matches is skipped |
assets | take: list of pictures, fonts, attachments (default all). Only as the last step; each file's outputs lists every file written and the report |
properties | title, author, subject, keywords (an empty one empties it) |
bookmarks | contents (a contents page in front, default true), levels (1 to 6, default 2), title (the contents' title). A file with no headings is skipped |
repair | |
accessibility | action: check (the default), tag or fix; part: 1 (PDF/UA-1, the default) or 2 (PDF/UA-2); fail (check: a file that does not pass fails); lang (tag, fix: the document's language, such as "en-GB"); title (tag, fix); mark (fix: marked PDF/UA when nothing else is left). The log of each file says the verdict and what is left to do by hand |
Examples
Make every scan in a folder searchable, as text files next to them:
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"steps": [{"do": "ocr"}, {"do": "text"}], "inputs": ["D:\\Scans"], "output": {"suffix": ""}}' \
http://127.0.0.1:7431/v1/jobs
Wait for a job in Python:
import json, time, urllib.request
TOKEN, BASE = "<the token>", "http://127.0.0.1:7431"
def call(method, path, body=None):
req = urllib.request.Request(BASE + path, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.load(r)
job = call("POST", "/v1/jobs", {"recipe": "Invoices in", "inputs": [r"D:\Scans\2026-10"],
"output": {"folder": r"D:\Invoices\2026-10"}})
while (state := call("GET", "/v1/jobs/" + job["id"]))["state"] in ("waiting", "running", "stopping"):
time.sleep(2)
for f in state["files"]:
print(f["state"], f["input"], f.get("message", ""))
Always check redacted copies before you send them.