DocWise.ai API and MCP server
Last updated 2026-11-01
The DocWise.ai API translates documents from your own code, with the same quality tiers, prices and limits as the app. A remote MCP server gives AI agents (Claude, Cursor and other MCP clients) the same abilities.
The API is part of the Pro and Ultra plans. Machine-readable spec: /api/v1/openapi.json (OpenAPI 3.1).
Quick start
- On a Pro or Ultra plan, open API keys and create a key. It starts with
sk_and is shown once: copy it into your secret store. - Upload, translate, download:
export DOCWISE_KEY=sk_...
API=https://docwise.ai/api/v1
# 1. Declare the file: you get a presigned upload URL
curl -s -X POST $API/documents \
-H "Authorization: Bearer $DOCWISE_KEY" -H "Content-Type: application/json" \
-d '{"filename":"report.pdf","bytes":'$(wc -c < report.pdf)',"mime":"application/pdf"}'
# → {"document":{"id":"DOC_ID",...},"method":"PUT","url":"https://…","expires_in":3600}
# 2. Upload the bytes straight to storage (curl sets Content-Length)
curl -s -X PUT -T report.pdf "UPLOAD_URL"
# 3. Finish: the file is read and priced
curl -s -X POST $API/documents/DOC_ID/complete -H "Authorization: Bearer $DOCWISE_KEY"
# → {"document":{...,"pages":12},"quote":{"pages":12,"tiers":[{"tier":"standard","credits":24,...}],...}}
# 4. Translate (credits are held now, settled when the job ends)
curl -s -X POST $API/translations \
-H "Authorization: Bearer $DOCWISE_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: report-de-2026-11-01" \
-d '{"document_id":"DOC_ID","tgt_lang":"de","tier":"standard"}'
# → {"id":"JOB_ID","status":"queued","billing":{"held_credits":24,...},...}
# 5. Poll until done, then get download links (valid 5 minutes)
curl -s $API/translations/JOB_ID -H "Authorization: Bearer $DOCWISE_KEY"
curl -s $API/translations/JOB_ID/outputs -H "Authorization: Bearer $DOCWISE_KEY"
Authentication
Send the key in every request: Authorization: Bearer sk_.... Keys belong to your account and see only your documents and translations; anyone else's id answers 404. Call the API from your server, never from a web page: the API sends no CORS headers and cookies are never accepted.
- Lost a key? Delete it on the API keys page and create a new one. Deleted keys stop working at once (
401). - If your subscription ends, your keys answer
403 plan_requireduntil you subscribe again.
The translation flow
POST /documentswithfilename, the exact size inbytesandmime. Same formats and size limits as the app (GET /formats). Up to 16 MiB you get oneurl; above that,partsto upload in order, each to its own URL,part_sizebytes each (the last one shorter).PUTthe bytes with exactly thatContent-Lengthwithinexpires_inseconds (1 hour). URLs expired?POST /documents/{id}/upload-urls.POST /documents/{id}/completereads the document and returns{ document, quote }.quote.tiers[]lists every tier with its price in credits andavailableon your plan.GET /documents/{id}/quoteprices it again later.POST /translationswithdocument_id,tgt_lang(fromGET /languages),tierand optionallyglossary_id,src_lang,outputsandtrial: true(first 3 pages only; a later full run of the same document, language and tier does not pay for them again).- Poll
GET /translations/{id}every 2–5 seconds.statusgoesqueued → parsing → translating → reviewing → rendering → done(orfailed,canceled);progressruns from 0 to 1. GET /translations/{id}/outputsreturns a presigned URL per output file, valid 5 minutes;GET /translations/{id}/zipstreams them all as one ZIP.
POST /translations/{id}/cancel stops a job; DELETE /documents/{id} deletes the file and every output at once (running jobs are canceled and refunded).
Credits and tiers
1 credit = 1 page on Fast. A page is one PDF page or slide, or 2,000 characters of text.
economy(Fast): 1 credit per pagestandard(Balanced): 2 credits per pagepro(Best): 5 credits per pageflagship(Expert): 10 credits per page, Ultra plan only
Creating a translation holds pages × credits per page. When it ends you are charged for what was delivered, never more than the hold; a failed or canceled job returns everything. billing in each translation shows held_credits, charged_credits and refunded_credits.
Idempotency
Send Idempotency-Key: <8–128 letters, digits, . _ : -> with POST /translations. Retrying with the same key and body within 24 hours returns the same translation (header Idempotency-Replayed: true) and never holds credits twice. The same key with a different body answers 409 idempotency_conflict. A request that failed did nothing, so it may be retried with the same key.
Limits
- Files: Pro and Ultra take files up to 200 MB and documents up to 1,000 pages. Documents are kept 30 days (Pro) or 90 days (Ultra).
- Rate limits per API key and operation: one call per 200 ms (account, languages, formats), 300 ms (lists, document, quote), 500 ms (translation status, outputs) and 1 second (upload, complete, translate, cancel, delete, ZIP). Over the limit:
429 too_many_requestswith aRetry-Afterheader in seconds. - At most 10 unfinished uploads at a time. Download links last 5 minutes, upload links 1 hour.
Errors
Every error is JSON with a real HTTP status:
{ "error": { "code": "insufficient_credits", "message": "Not enough credits" } }
400 invalid_request: a missing or malformed field (the message names it)401 unauthorized: no key, or a wrong, deleted or expired one (failed attempts from one IP are throttled: a retry within 200 ms answers429)402 insufficient_credits: top up or pick a cheaper tier403 plan_required: the account is not on Pro or Ultra;403 tier_unavailable: that tier is not on your plan404 not_found: no such id in your account409 upload_incomplete,document_not_ready,not_ready,idempotency_conflict,idempotency_in_progress413 file_too_large,415 unsupported_format,422 too_many_pages,file_encrypted,file_corrupt429 too_many_requests,too_many_uploads5xx internal,engine_timeout: retry later; a job that could not start returned its credits
Python
import os, time, requests
API = "https://docwise.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['DOCWISE_KEY']}"}
def call(method, path, **kw):
while True:
r = requests.request(method, API + path, headers={**H, **kw.pop("headers", {})}, **kw)
if r.status_code != 429:
break
time.sleep(int(r.headers.get("Retry-After", "1")))
if r.status_code >= 400:
raise RuntimeError(r.json()["error"])
return r.json()
data = open("report.pdf", "rb").read()
up = call("POST", "/documents", json={"filename": "report.pdf", "bytes": len(data), "mime": "application/pdf"})
requests.put(up["url"], data=data).raise_for_status() # files ≤ 16 MiB: one PUT
doc = call("POST", f"/documents/{up['document']['id']}/complete")
print("pages:", doc["quote"]["pages"])
job = call("POST", "/translations", json={"document_id": doc["document"]["id"], "tgt_lang": "de", "tier": "standard"},
headers={"Idempotency-Key": f"report-de-{doc['document']['id']}"})
while job["status"] not in ("done", "failed", "canceled"):
time.sleep(3)
job = call("GET", f"/translations/{job['id']}")
for out in call("GET", f"/translations/{job['id']}/outputs")["outputs"]:
open(out["filename"], "wb").write(requests.get(out["url"]).content)
JavaScript (Node 18+)
import { readFile, writeFile } from 'node:fs/promises';
const API = 'https://docwise.ai/api/v1';
const H = { Authorization: `Bearer ${process.env.DOCWISE_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(method, path, body, headers = {}) {
for (;;) {
const res = await fetch(API + path, {
method,
headers: { ...H, ...headers, ...(body ? { 'Content-Type': 'application/json' } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 429) { await sleep(1000 * Number(res.headers.get('retry-after') || 1)); continue; }
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
return json;
}
}
const bytes = await readFile('report.pdf');
const up = await call('POST', '/documents', { filename: 'report.pdf', bytes: bytes.length, mime: 'application/pdf' });
await fetch(up.url, { method: 'PUT', body: bytes });
const { document, quote } = await call('POST', `/documents/${up.document.id}/complete`);
console.log('credits on Balanced:', quote.tiers.find((t) => t.tier === 'standard').credits);
let job = await call('POST', '/translations', { document_id: document.id, tgt_lang: 'de', tier: 'standard' },
{ 'Idempotency-Key': `report-de-${document.id}` });
while (!['done', 'failed', 'canceled'].includes(job.status)) {
await sleep(3000);
job = await call('GET', `/translations/${job.id}`);
}
const { outputs } = await call('GET', `/translations/${job.id}/outputs`);
for (const out of outputs)
await writeFile(out.filename, Buffer.from(await (await fetch(out.url)).arrayBuffer()));
MCP server
Endpoint: https://docwise.ai/api/mcp (Streamable HTTP), with the same API key as a Bearer token. Tools: get_account, list_languages, list_documents, upload_document, complete_upload, quote_document, translate_document, get_translation, get_download_links, cancel_translation, list_glossaries.
upload_document takes the file as base64 up to 10 MB; for bigger files it returns a presigned upload URL your client PUTs to, then complete_upload. The server never fetches URLs you give it. Agents are told to confirm the credit cost with you before translate_document.
Claude Code
claude mcp add --transport http docwise https://docwise.ai/api/mcp \
--header "Authorization: Bearer sk_..."
Cursor (~/.cursor/mcp.json or .cursor/mcp.json in a project)
{
"mcpServers": {
"docwise": {
"url": "https://docwise.ai/api/mcp",
"headers": { "Authorization": "Bearer sk_..." }
}
}
}
Claude Desktop (Settings → Developer → Edit Config, claude_desktop_config.json; uses the mcp-remote bridge, needs Node.js)
{
"mcpServers": {
"docwise": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://docwise.ai/api/mcp", "--header", "Authorization:${DOCWISE_AUTH}"],
"env": { "DOCWISE_AUTH": "Bearer sk_..." }
}
}
}
Keep keys out of shared repositories: prefer an environment variable or your client's secret store.