# PDF editor API for AI agents

Free. No API key. Base URL: https://buyexamvouchers.com/v1/pdf. Machine-readable schema: https://buyexamvouchers.com/openapi.json
PDFs up to 10 MB and 300 pages. Files are processed in memory and never stored.

## Workflow that cannot silently go wrong
1. POST /v1/pdf/inspect with the PDF. You get sha256, pages (size, rotation), text runs (one line of text each, with id, exact text, position and font) and form fields.
2. POST /v1/pdf/edit with the same PDF and a plan. Copy expectedSha256 from step 1, and copy each run's id and text into runId and expectedText.
3. Use "dryRun": true first when you are unsure. It validates and verifies everything but returns no file.
4. Check "ok". If false, "errors" lists every problem, each with a stable code and the operation index. Nothing was changed.

## Guarantees
- A plan applies completely or not at all.
- After editing, the output is reopened and checked: page count and rotation, every untouched character still in the same position, new text reads back exactly, form values read back. If a check fails you get VERIFY_FAILED and no file.
- Replaced, removed and whited-out text is removed from the file, not just hidden. Deleted pages are removed from the file.

## Requests
- multipart/form-data: part "file" (PDF), part "plan" (JSON), one more file part per image or PDF asset, named by asset id.
- application/json: {"fileBase64": "...", "plan": {...}, "assets": {"id": "base64"}}.
- Add header "Accept: application/pdf" to /v1/pdf/edit to receive the PDF bytes directly. Otherwise JSON with pdfBase64.
- MCP (Streamable HTTP): POST https://buyexamvouchers.com/v1/pdf/mcp with tools pdf_inspect and pdf_edit.

## Coordinates
PDF points (1/72 inch) from the top-left of the page as displayed. Page numbers start at 1 and always mean the original document, even when arrangePages moves pages.

## Operations (plan.ops)
- {"type":"replaceText","page":1,"runId":"p1r2","expectedText":"Old line","text":"New line","overflow":"error"} overflow is error (default), shrink or allow.
- {"type":"findReplace","find":"2025","replace":"2026","expectedCount":3,"matchCase":true}
- {"type":"removeText","page":1,"runId":"p1r2","expectedText":"Old line"}
- {"type":"addText","page":1,"x":72,"y":72,"text":"Hello","size":12,"family":"sans","bold":false,"italic":false,"color":"#000000","maxWidth":300,"align":"left"}
- {"type":"addImage","page":1,"x":72,"y":72,"width":120,"height":40,"asset":"sig"} PNG or JPEG.
- {"type":"whiteout","page":1,"x":72,"y":72,"width":200,"height":20}
- {"type":"shape","page":1,"kind":"rectangle","x":72,"y":72,"width":100,"height":50,"stroke":"#ff0000","strokeWidth":1} kind is rectangle or ellipse. fill is optional.
- {"type":"line","page":1,"from":[72,72],"to":[200,72],"arrow":true}
- {"type":"freehand","page":1,"points":[[72,72],[80,90]]}
- {"type":"mark","page":1,"kind":"highlight","runId":"p1r2","expectedText":"Old line"} kind is highlight, underline or strikeout. Use "rect" instead of runId for an area.
- {"type":"link","page":1,"x":72,"y":72,"width":100,"height":14,"url":"https://example.com"} or "toPage":3.
- {"type":"setField","name":"Full name","value":"Ada Lovelace"}
- {"type":"arrangePages","pages":[{"from":2},{"from":1,"rotate":90},{"blank":{"width":612,"height":792}},{"asset":"other","page":1}]} The list is the final document. Pages left out are deleted.

## Error codes
INVALID_PDF, ENCRYPTED_PDF, TOO_LARGE, INVALID_PLAN, DOC_CHANGED, PAGE_NOT_FOUND, RUN_NOT_FOUND, TEXT_MISMATCH, RUN_NOT_EDITABLE, MATCH_COUNT_MISMATCH, DUPLICATE_TARGET, UNSUPPORTED_CHARACTER, TEXT_OVERFLOW, OUT_OF_BOUNDS, ASSET_NOT_FOUND, INVALID_ASSET, FIELD_NOT_FOUND, FIELD_VALUE_INVALID, VERIFY_FAILED, RATE_LIMITED, BUSY.

## Limits
Typing supports Latin, Greek and Cyrillic. Password-protected PDFs are refused. Text inside reusable graphic layers cannot be replaced directly (run.editable is false): cover it with whiteout and add text. Rate limit: 40 requests per 10 minutes per IP.
