ChangeMyPDF API
A small, focused REST API for automating PDF and document workflows. Authenticate with an API key, call any tool, and track usage from your own code.
Getting started
The ChangeMyPDF API uses standard HTTP verbs, JSON bodies, and bearer-token auth. Base URL: https://changemypdf.com/api (or http://localhost:3000/api for local dev).
Three steps to your first request:
- Create an account (free).
- Go to Settings → API and click New key. Copy the secret — it's shown exactly once.
- Run the request below:
curl https://changemypdf.com/api/runs?limit=5 \
-H "Authorization: Bearer dk_live_..."Authentication
Send your key as a bearer token on every request:
Authorization: Bearer dk_live_abc12345_<48-char secret>Key format
Keys look like dk_live_abc12345_secret. The first 16 characters are a public prefix you can show in dashboards or logs — only the trailing 48 characters are sensitive.
Revoking keys
From Settings → API, click Revoke next to any key. Revocation is instant; clients using a revoked key immediately get 401 unauthorized.
Errors & rate limits
Errors follow the standard HTTP status codes:
400— invalid input (your JSON or query failed validation).401— missing or invalid key.404— resource doesn't exist.410— resource expired (e.g. chat session older than 30 min).413— payload too large.422— couldn't process (e.g. unreadable PDF).429— rate-limited. Backoff and retry.5xx— our problem. Retries are safe — every endpoint is idempotent on identical input.
Bodies are JSON: { "error": "...", "detail": "..." }. Successful responses use HTTP 200 or 202.
Tool runs
Log a tool-run from your own code or pull recent history. Both routes are also used by the web app — your runs from the browser and from the API end up in the same dashboard.
List runs
/api/runs?limit=10curl https://changemypdf.com/api/runs?limit=10 \
-H "Authorization: Bearer dk_live_..."Log a run
/api/runsUseful for self-hosted tools or pipelines that want to surface on your dashboard. Field validation uses zod; tool must be a known slug.
curl -X POST https://changemypdf.com/api/runs \
-H "Authorization: Bearer dk_live_..." \
-H "Content-Type: application/json" \
-d '{
"tool": "merge-pdf",
"status": "succeeded",
"durationMs": 1234,
"fileCount": 2,
"inputBytes": 2400000,
"outputBytes": 2100000
}'AI · Summarize
/api/ai/summarizeMultipart upload of a PDF (max 20 MB). Returns a JSON object with a TL;DR, key-point bullets, and topic tags. With ANTHROPIC_API_KEY on the server, this uses Claude Haiku; otherwise a local heuristic. Responses always include a provider field so you can branch.
curl -X POST https://changemypdf.com/api/ai/summarize \
-H "Authorization: Bearer dk_live_..." \
-F "file=@report.pdf"AI · Chat with PDF
Two-phase: upload a doc to get a docId, then chat against it.
Upload
/api/ai/chatContent-Type: multipart/form-data. The doc is held server-side for 30 minutes.
curl -X POST https://changemypdf.com/api/ai/chat \
-H "Authorization: Bearer dk_live_..." \
-F "file=@contract.pdf"Chat (non-streaming)
/api/ai/chatContent-Type: application/json. Body: docId, message, optional history (max 16 turns).
curl -X POST https://changemypdf.com/api/ai/chat \
-H "Authorization: Bearer dk_live_..." \
-H "Content-Type: application/json" \
-d '{"docId":"...", "message":"What is the term?"}'Chat (streaming)
/api/ai/chat?stream=1Server-Sent Events. Three event types: delta (text chunks),done (final with provider info), error.
curl -N -X POST 'https://changemypdf.com/api/ai/chat?stream=1' \
-H "Authorization: Bearer dk_live_..." \
-H "Content-Type: application/json" \
-d '{"docId":"...", "message":"Summarise section 2"}'AI · Translate
/api/ai/translateTranslates a PDF into one of 20 supported languages and returns a brand-new PDF (binary) with script-aware fonts (Latin, Cyrillic, Arabic, Hebrew, Devanagari, Thai, CJK). Layout-preservation is on the Premium roadmap; today's output is clean paginated text.
curl -X POST https://changemypdf.com/api/ai/translate \
-H "Authorization: Bearer dk_live_..." \
-F "file=@report.pdf" \
-F "targetLang=Japanese" \
--output translated.pdfThe response body is application/pdf. The X-Translation-Meta header is URL-encoded JSON: { targetLang, modelLabel, provider }.
Webhooks
Receive HTTP POSTs on your own endpoint whenever interesting events happen. Manage them at Settings → API → Outbound webhooks. Use the Send test button on any row to fire a sample event and confirm your signature verification works.
Events
tool.run.succeeded— fired after every successful tool run logged to your account.tool.run.failed— fired when a run is logged withstatus: "failed".
Payload shape
{
"id": "evt_a1b2c3d4e5f6",
"type": "tool.run.succeeded",
"createdAt": "2026-03-14T10:32:00.123Z",
"data": {
"id": "run_abc123",
"tool": "merge-pdf",
"status": "succeeded",
"durationMs": 1234,
"inputBytes": 4500000,
"outputBytes": 3800000,
"fileCount": 3,
"errorCode": null,
"createdAt": "2026-03-14T10:32:00.000Z"
}
}Signature header
Every request includes X-ChangeMyPDF-Signature: t=<unix-ms>,v1=<hmac>where hmac is HMAC-SHA256(signingSecret, t + "." + rawBody), hex-encoded. Use a constant-time comparison (e.g. crypto.timingSafeEqual) to prevent timing attacks. Reject requests where t is more than 5 minutes off to defeat replay attacks.
Verify (Node.js)
import crypto from 'node:crypto';
// Express-style handler. Body MUST be the raw bytes — many frameworks parse
// JSON first; you need the un-parsed string for signature verification.
app.post('/changemypdf', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-changemypdf-signature'] || '';
const [ts, v1] = sig.split(',').map((s) => s.split('=')[1]);
const expected = crypto
.createHmac('sha256', process.env.CHANGEMYPDF_WEBHOOK_SECRET)
.update(`${ts}.${req.body.toString()}`)
.digest('hex');
if (
!v1 ||
!crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
) {
return res.status(401).send('bad signature');
}
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
return res.status(401).send('stale');
}
const event = JSON.parse(req.body.toString());
// Acknowledge fast (return 2xx in under 5 seconds) — long work goes in a queue.
res.status(202).send();
enqueueBackgroundJob(event);
});Retries & auto-disable
We POST with a 5-second timeout. Anything outside the 2xx range counts as a failure. The bucket auto-disables after 20 consecutive failures so a dead endpoint doesn't churn against you forever. A successful Send test from the settings UI re-enables a disabled hook.
We do not retry failed deliveries today — the customer's POST /api/runscall returns 200 regardless, and the dispatcher is fire-and-forget. Retry queue work is on the Business roadmap.