ChangeMyPDF
API v1

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:

  1. Create an account (free).
  2. Go to Settings → API and click New key. Copy the secret — it's shown exactly once.
  3. 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

GET/api/runs?limit=10
curl https://changemypdf.com/api/runs?limit=10 \
  -H "Authorization: Bearer dk_live_..."

Log a run

POST/api/runs

Useful 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

POST/api/ai/summarize

Multipart 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

POST/api/ai/chat

Content-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)

POST/api/ai/chat

Content-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)

POST/api/ai/chat?stream=1

Server-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

POST/api/ai/translate

Translates 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.pdf

The 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 with status: "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.


Need a tool, not the API?
Browse the 29 web tools →
Ready to integrate?
Create an API key →