Back to home

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

  1. 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.
  2. 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.

The translation flow

  1. POST /documents with filename, the exact size in bytes and mime. Same formats and size limits as the app (GET /formats). Up to 16 MiB you get one url; above that, parts to upload in order, each to its own URL, part_size bytes each (the last one shorter).
  2. PUT the bytes with exactly that Content-Length within expires_in seconds (1 hour). URLs expired? POST /documents/{id}/upload-urls.
  3. POST /documents/{id}/complete reads the document and returns { document, quote }. quote.tiers[] lists every tier with its price in credits and available on your plan. GET /documents/{id}/quote prices it again later.
  4. POST /translations with document_id, tgt_lang (from GET /languages), tier and optionally glossary_id, src_lang, outputs and trial: true (first 3 pages only; a later full run of the same document, language and tier does not pay for them again).
  5. Poll GET /translations/{id} every 2–5 seconds. status goes queued → parsing → translating → reviewing → rendering → done (or failed, canceled); progress runs from 0 to 1.
  6. GET /translations/{id}/outputs returns a presigned URL per output file, valid 5 minutes; GET /translations/{id}/zip streams 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.

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

Errors

Every error is JSON with a real HTTP status:

{ "error": { "code": "insufficient_credits", "message": "Not enough 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.