API
Everything the site does, it also does over HTTP. One key, one request, one job id to poll. The examples below are copy-paste ready: swap in your key and they run.
All our keys start with prk_. That prefix lets GitHub's secret scanning warn you if one is ever published by mistake.
Two-minute start
No key required. This is the first call to try when something looks wrong.
curl https://pdfremakeit.app/v1/statusconst r = await fetch("https://pdfremakeit.app/v1/status");
console.log(await r.json());
// { "status": "operational", "capacity": "normal" }import urllib.request, json
with urllib.request.urlopen("https://pdfremakeit.app/v1/status") as r:
print(json.load(r))
# {'status': 'operational', 'capacity': 'normal'}Authentication
Every call carries your key in the Authorization header. A key is a password: never ship it in browser-side code, never commit it to a public repository.
Base URL : https://pdfremakeit.app
Authorization: Bearer prk_…API keys
One key per use: a compromised environment can be revoked on its own.
You can hold up to 10 active keys.
Recent calls
No calls recorded yet. Calls show up here a few seconds later.
Call details are kept for thirty days.
Endpoints
| Call | Key | What it does |
|---|---|---|
GET /v1 | — | Index of endpoints. |
GET /v1/status | — | Service health and capacity. |
GET /v1/tools | — | Catalogue of server-side tools and accepted formats. |
GET /v1/account | yes | Plan, limits and usage. |
GET /v1/logs | yes | Recent calls made with your keys. |
POST /v1/tools/{outil} | yes | Submit a document. Returns a job. |
GET /v1/jobs/{id} | yes | Job status. |
GET /v1/jobs/{id}/content | yes | Download the result. |
GET /v1/workflows | yes | Your saved workflows. |
POST /v1/workflows | yes | Save a workflow. |
DELETE /v1/workflows/{id} | yes | Delete a workflow. Past runs are kept. |
POST /v1/workflows/{id}/run | yes | Run a workflow on a document. |
GET /v1/runs | yes | Your recent runs. |
GET /v1/runs/{id} | yes | Run status, step by step. |
GET /v1/runs/{id}/content | yes | Download the final result. |
POST /v1/runs/{id}/replay | yes | Run an execution again on the same document. |
POST /v1/ai/summarize | yes | Summarise text. |
POST /v1/ai/translate | yes | Translate text. |
Convert a document to PDF
The document goes in the request body, raw. The original name travels in the X-Source-Name header: its extension decides how the file is handled.
# 1. Envoyer le document. La réponse porte l'identifiant du travail.
curl -X POST https://pdfremakeit.app/v1/tools/office \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "X-Source-Name: rapport.docx" \
--data-binary @rapport.docx
# 2. Interroger le travail jusqu'à "done".
curl https://pdfremakeit.app/v1/jobs/JOB_ID \
-H "Authorization: Bearer prk_VOTRE_CLE"
# 3. Récupérer le PDF.
curl -L https://pdfremakeit.app/v1/jobs/JOB_ID/content \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-o rapport.pdfimport { readFile, writeFile } from "node:fs/promises";
const CLE = "prk_VOTRE_CLE";
const entetes = { Authorization: `Bearer ${CLE}` };
const depart = await fetch("https://pdfremakeit.app/v1/tools/office", {
method: "POST",
headers: { ...entetes, "X-Source-Name": "rapport.docx" },
body: await readFile("rapport.docx"),
});
const { id } = await depart.json();
// Un travail serveur est asynchrone : on interroge jusqu'à ce qu'il aboutisse.
let etat;
do {
await new Promise((r) => setTimeout(r, 1500));
etat = await (await fetch(`https://pdfremakeit.app/v1/jobs/${id}`, { headers: entetes })).json();
} while (etat.status === "queued" || etat.status === "running");
if (etat.status === "error") throw new Error(etat.error);
const pdf = await fetch(`https://pdfremakeit.app/v1/jobs/${id}/content`, { headers: entetes });
await writeFile("rapport.pdf", Buffer.from(await pdf.arrayBuffer()));import time, requests
CLE = "prk_VOTRE_CLE"
entetes = {"Authorization": f"Bearer {CLE}"}
with open("rapport.docx", "rb") as f:
depart = requests.post(
"https://pdfremakeit.app/v1/tools/office",
headers={**entetes, "X-Source-Name": "rapport.docx"},
data=f,
)
depart.raise_for_status()
identifiant = depart.json()["id"]
# Un travail serveur est asynchrone : on interroge jusqu'à ce qu'il aboutisse.
while True:
time.sleep(1.5)
etat = requests.get(f"https://pdfremakeit.app/v1/jobs/{identifiant}", headers=entetes).json()
if etat["status"] not in ("queued", "running"):
break
if etat["status"] == "error":
raise RuntimeError(etat["error"])
pdf = requests.get(f"https://pdfremakeit.app/v1/jobs/{identifiant}/content", headers=entetes)
open("rapport.pdf", "wb").write(pdf.content)Run OCR on a scan
Same flow as conversion: only the tool and its settings change. The language goes in the query string.
curl -X POST "https://pdfremakeit.app/v1/tools/ocr?langues=fra" \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "X-Source-Name: scan.pdf" \
--data-binary @scan.pdfconst depart = await fetch("https://pdfremakeit.app/v1/tools/ocr?langues=fra", {
method: "POST",
headers: {
Authorization: `Bearer ${CLE}`,
"X-Source-Name": "scan.pdf",
},
body: await readFile("scan.pdf"),
});
const { id, links } = await depart.json();
console.log(links.self); // /v1/jobs/...with open("scan.pdf", "rb") as f:
depart = requests.post(
"https://pdfremakeit.app/v1/tools/ocr",
params={"langues": "fra"},
headers={**entetes, "X-Source-Name": "scan.pdf"},
data=f,
)
print(depart.json()["id"])Summarise text
Summarisation takes text, not a file: extract it first — with the OCR tool above, or from your own source.
curl -X POST https://pdfremakeit.app/v1/ai/summarize \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{
"text": "Le rapport annuel 2025 fait état d un chiffre d affaires de 184 000 euros...",
"length": "short",
"format": "bullets"
}'const r = await fetch("https://pdfremakeit.app/v1/ai/summarize", {
method: "POST",
headers: {
Authorization: `Bearer ${CLE}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, length: "short", format: "bullets" }),
});
const { text: resume, partial } = await r.json();
// `partial` vaut true si le traitement s'est interrompu : le texte reste
// utilisable, mais il est incomplet.
console.log(resume);r = requests.post(
"https://pdfremakeit.app/v1/ai/summarize",
headers={**entetes, "Content-Type": "application/json"},
json={"text": texte, "length": "short", "format": "bullets"},
)
resultat = r.json()
# `partial` vaut True si le traitement s'est interrompu : le texte reste
# utilisable, mais il est incomplet.
print(resultat["text"])Translate as a stream
With "stream": true, the response is an NDJSON stream: one JSON object per line. That is what lets you display the text as it is written.
curl -N -X POST https://pdfremakeit.app/v1/ai/translate \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"text": "Bonjour le monde.", "targetLanguage": "en", "stream": true}'
# {"t":"debut","blocs":1,...}
# {"t":"texte","v":"Hello"}
# {"t":"texte","v":" world."}
# {"t":"fin","caracteres":12}const r = await fetch("https://pdfremakeit.app/v1/ai/translate", {
method: "POST",
headers: {
Authorization: `Bearer ${CLE}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, targetLanguage: "en", stream: true }),
});
const lecteur = r.body.pipeThrough(new TextDecoderStream()).getReader();
let reste = "";
for (;;) {
const { done, value } = await lecteur.read();
if (done) break;
reste += value;
let saut;
while ((saut = reste.indexOf("\n")) !== -1) {
const ligne = reste.slice(0, saut);
reste = reste.slice(saut + 1);
if (!ligne) continue;
const evenement = JSON.parse(ligne);
if (evenement.t === "texte") process.stdout.write(evenement.v);
}
}import json, requests
with requests.post(
"https://pdfremakeit.app/v1/ai/translate",
headers={**entetes, "Content-Type": "application/json"},
json={"text": texte, "targetLanguage": "en", "stream": True},
stream=True,
) as r:
for ligne in r.iter_lines():
if not ligne:
continue
evenement = json.loads(ligne)
if evenement["t"] == "texte":
print(evenement["v"], end="", flush=True)Chain several tools
A workflow is saved once, then runs in a single call: each step's result becomes the next step's input. Paid plans only. Five steps at most, and only the first one may refuse PDF.
# 1. Enregistrer l'enchaînement. Rendu une fois pour toutes.
curl -X POST https://pdfremakeit.app/v1/workflows \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{"name":"Courrier scanné",
"steps":[{"tool":"ocr","settings":{"langues":"fra"}},
{"tool":"pdfa","settings":{"niveau":"2b"}}]}'
# 2. Le lancer sur un document. Le quota des deux étapes est réservé ici.
curl -X POST https://pdfremakeit.app/v1/workflows/WORKFLOW_ID/run \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-H "X-Source-Name: courrier.pdf" \
--data-binary @courrier.pdf
# 3. Suivre, puis récupérer le PDF de la dernière étape.
curl https://pdfremakeit.app/v1/runs/RUN_ID -H "Authorization: Bearer prk_VOTRE_CLE"
curl -L https://pdfremakeit.app/v1/runs/RUN_ID/content \
-H "Authorization: Bearer prk_VOTRE_CLE" \
-o archive.pdfimport { readFile, writeFile } from "node:fs/promises";
const chaine = await (
await fetch("https://pdfremakeit.app/v1/workflows", {
method: "POST",
headers: { ...entetes, "Content-Type": "application/json" },
body: JSON.stringify({
name: "Courrier scanné",
steps: [
{ tool: "ocr", settings: { langues: "fra" } },
{ tool: "pdfa", settings: { niveau: "2b" } },
],
}),
})
).json();
const depart = await fetch(`https://pdfremakeit.app/v1/workflows/${chaine.id}/run`, {
method: "POST",
headers: { ...entetes, "X-Source-Name": "courrier.pdf" },
body: await readFile("courrier.pdf"),
});
let execution = await depart.json();
// Une étape peut durer plusieurs minutes : on interroge, on n'attend pas.
while (execution.status === "running") {
await new Promise((f) => setTimeout(f, 3000));
execution = await (
await fetch(`https://pdfremakeit.app/v1/runs/${execution.id}`, { headers: entetes })
).json();
const etape = Math.min(execution.step + 1, execution.totalSteps);
console.log(`étape ${etape} / ${execution.totalSteps}`);
}
if (execution.status !== "done") throw new Error(execution.error);
const pdf = await fetch(`https://pdfremakeit.app/v1/runs/${execution.id}/content`, {
headers: entetes,
});
await writeFile("archive.pdf", Buffer.from(await pdf.arrayBuffer()));import time, requests
chaine = requests.post(
"https://pdfremakeit.app/v1/workflows",
headers={**entetes, "Content-Type": "application/json"},
json={
"name": "Courrier scanné",
"steps": [
{"tool": "ocr", "settings": {"langues": "fra"}},
{"tool": "pdfa", "settings": {"niveau": "2b"}},
],
},
).json()
with open("courrier.pdf", "rb") as f:
execution = requests.post(
f"https://pdfremakeit.app/v1/workflows/{chaine['id']}/run",
headers={**entetes, "X-Source-Name": "courrier.pdf"},
data=f,
).json()
while execution["status"] == "running":
time.sleep(3)
execution = requests.get(
f"https://pdfremakeit.app/v1/runs/{execution['id']}", headers=entetes
).json()
etape = min(execution["step"] + 1, execution["totalSteps"])
print(f"etape {etape} / {execution['totalSteps']}")
if execution["status"] != "done":
raise SystemExit(execution["error"])
pdf = requests.get(
f"https://pdfremakeit.app/v1/runs/{execution['id']}/content", headers=entetes
)
open("archive.pdf", "wb").write(pdf.content)Run an execution again
Without re-uploading the document: it is still with us for as long as your plan's retention runs. After that, the call answers source_expired.
curl -X POST https://pdfremakeit.app/v1/runs/RUN_ID/replay \
-H "Authorization: Bearer prk_VOTRE_CLE"const rejeu = await fetch(`https://pdfremakeit.app/v1/runs/${execution.id}/replay`, {
method: "POST",
headers: entetes,
});
if (rejeu.status === 410) {
// Le document d'origine a été purgé : il faut le renvoyer.
console.log("source expirée, relancer le workflow avec le fichier");
} else {
console.log("nouvelle exécution :", (await rejeu.json()).id);
}rejeu = requests.post(
f"https://pdfremakeit.app/v1/runs/{execution['id']}/replay", headers=entetes
)
if rejeu.status_code == 410:
print("source expiree, relancer le workflow avec le fichier")
else:
print("nouvelle execution :", rejeu.json()["id"])Read your quota
Call it before a batch rather than after a refusal: `resetsAt` says when the counter resets.
curl https://pdfremakeit.app/v1/account \
-H "Authorization: Bearer prk_VOTRE_CLE"const compte = await (
await fetch("https://pdfremakeit.app/v1/account", { headers: entetes })
).json();
const { used, limit } = compte.usage.serverJobs;
console.log(`${used} / ${limit} travaux aujourd'hui`);compte = requests.get("https://pdfremakeit.app/v1/account", headers=entetes).json()
usage = compte["usage"]["serverJobs"]
print(f"{usage['used']} / {usage['limit']} travaux aujourd'hui")Errors
Every error has the same shape. Test the code, never the message: the code is part of the contract, the message may be rewritten.
{
"error": {
"code": "quota_exceeded",
"message": "The daily job quota for this account is exhausted.",
"plan": "libre",
"used": 3,
"limit": 3,
"resetsAt": 1787011200000,
"documentation": "https://pdfremakeit.app/api#quota_exceeded"
}
}| code | HTTP | What happened | What to do |
|---|---|---|---|
missing_api_key | 401 | No key was sent. | Add the Authorization: Bearer header. |
invalid_api_key | 401 | Unknown or revoked key. | Check the key, or create a new one. |
rate_limited | 429 | Too many calls this minute. | Wait for the Retry-After header, then resume. |
quota_exceeded | 429 | Daily quota exhausted. | Resume at the time given by resetsAt, or upgrade the plan. |
too_many_concurrent_jobs | 429 | Too many jobs running at once. | Wait for a job to finish before starting another. |
unknown_tool | 404 | No such tool. | See GET /v1/tools. |
unsupported_input | 415 | The tool does not accept this extension. | Check the accepts field in the response. |
missing_filename | 400 | Original file name missing. | Add X-Source-Name, or ?filename=. |
empty_body | 400 | Empty request body. | Send the document as the raw body. |
file_too_large | 413 | File over the size limit. | See maxUploadBytes in the response. |
invalid_request | 400 | Malformed JSON body. | Check the text field. |
text_too_short | 400 | Text too short — usually a scan with no text layer. | Run the document through OCR first. |
text_too_long | 413 | Text over the limit. | Split the document and combine the results. |
unsupported_language | 400 | Target language outside the model's range. | See the supported field in the response. |
job_not_ready | 409 | The job has not finished. | Poll /v1/jobs/{id} until "done". |
result_expired | 410 | Result deleted. | Run the job again; see your plan's retention. |
plan_required | 403 | Workflows are for paid plans only. | Upgrade the plan, or call the tools one at a time. |
invalid_workflow | 400 | Workflow refused: unknown step, too many steps, or a tool that does not accept PDF placed after the first step. | See the details field, which names the offending step. |
too_many_workflows | 409 | Maximum number of workflows reached. | Delete a workflow you no longer use. |
source_expired | 410 | The original document was purged: there is nothing left to replay. | Run the workflow again, sending the file. |
not_found | 404 | No such path or job. | Check the identifier. |
service_capacity | 503 | Service suspended while a traffic surge is absorbed. | Retry later; paid plans are served first. |
internal_error | 500 | Failure on our side. | Nothing was charged: retry. |
Quotas and rate limits
A key spends its account's quota: going through the API does not double what you are entitled to. Rate limits, however, are counted per key — your environments do not slow each other down.
- 60 requests per minute per key.
- Free plan: 3 jobs and 20,000 characters per day.
- Pro plan: 3,000 jobs and 20,000,000 characters per day.
Libraries
Both libraries handle waiting for a job, typed errors and token-by-token streaming. Neither is required: the API is perfectly pleasant with curl.
npm install @pdfremakeit/sdkpip install pdfremakeit