On this page
Short answer. To self-host, run paddlex --install serving once, then paddlex --serve --pipeline OCR. You get POST http://localhost:8080/ocr. It takes JSON with file (a URL or Base64) and fileType (0 = PDF, 1 = image). To use the hosted API instead, get an access token from AI Studio (this is the "PaddleOCR API key"). Set it as PADDLEOCR_ACCESS_TOKEN and call the Python, TypeScript or Go SDK, or run paddleocr api. Baidu's quota doc gives each user 3,000 pages per model per day.1279
Self-hosted or hosted: which one?#
| Self-hosted (PaddleX basic serving) | Official hosted API | |
|---|---|---|
| Setup | pip install + an inference engine; models download on first start | None beyond an AI Studio account and token |
| Where files go | Your machine only | Uploaded to Baidu's servers |
| Auth | None built in. Put it behind your own gateway. | Authorization: Bearer <token> |
| Call style | Synchronous: one POST returns the result | Asynchronous: submit a job, poll, download JSONL |
| Cost / limits | Your hardware. Basic serving handles the first 10 PDF pages unless you change the config. | 3,000 pages per model per day free. Page caps per file conflict between docs (see limits). |
| Models | Any pipeline: OCR, PP-StructureV3, PaddleOCR-VL, your own fine-tuned models | OCR: PP-OCRv5, PP-OCRv6. Parsing: PP-StructureV3, PaddleOCR-VL, -1.5, -1.6 |
| Browser calls | No CORS headers (we checked). Proxy it through your backend. | Not documented |
Rule of thumb: use the hosted API to prototype or for low volume when uploading files is acceptable. Self-host when documents are confidential, when you need more than the free quota, or when you want your own models. Not sure you need HTTP at all? The Python API needs no server, and PaddleOCR.js runs OCR inside the browser.
Self-hosted HTTP serving in five minutes#
PaddleOCR doesn't have its own HTTP server. The docs say it "recommends using PaddleX for serving". PaddleX is installed automatically with paddleocr. There are two options. Basic serving is easy to set up. High-stability serving runs on NVIDIA Triton Inference Server.1 Basic serving is a FastAPI app run by Uvicorn, so it is the "PaddleOCR FastAPI" server people search for. You don't have to write one.3
Install PaddleOCR and an engine#
bashpython -m pip install paddlepaddle==3.2.0 paddleocr==3.7.0Pin the engine. In our install tests, a bare
pip install paddlepaddle(3.3.1) crashes PaddleOCR 3.7.0 on CPU.Add the serving plugin#
bashpaddlex --install servingThis pip-installs the
servingextra of PaddleX: FastAPI, Uvicorn, Starlette, aiohttp,bce-python-sdk,filetype,pypdfium2,yarland a pinnedopencv-contrib-python==4.10.0.84. In our environment it took 18 seconds.34Start the server#
bashpaddlex --serve --pipeline OCRThe official doc says to expect output like this. Ours matched:1
textINFO: Started server process [63108] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)Check it#
bashcurl -s http://localhost:8080/health # {"logId":"9bce6482-...","errorCode":0,"errorMsg":"Healthy"}FastAPI also serves interactive docs at
/docsand the schema at/openapi.json. On our OCR server the schema listed exactly two paths:/healthand/ocr.4
Server options#
| Option | What it does | Default |
|---|---|---|
--pipeline | Registration name (OCR, PP-StructureV3, PaddleOCR-VL, …) or the path to a pipeline config file | required |
--device | Device to run on, e.g. cpu, gpu:0 | GPU if available, else CPU |
--host | Bind address | 0.0.0.0 |
--port | Port | 8080 |
--use_hpip | High-performance inference plugin | off |
--hpi_config | High-performance inference configuration | — |
The default binds to every interface with no authentication. On a cloud VM, 0.0.0.0:8080 is reachable by anyone who can reach the machine. Use --host 127.0.0.1 and put a reverse proxy with auth in front, or firewall the port.1
Endpoints and the request/response format#
Every pipeline uses JSON over POST. The path depends on the pipeline:25
Pipeline (--pipeline) | Endpoint | Result key |
|---|---|---|
OCR | POST /ocr | result.ocrResults[] |
PP-StructureV3 | POST /layout-parsing | result.layoutParsingResults[] |
PaddleOCR-VL (and -1.5, -1.6) | POST /layout-parsing | result.layoutParsingResults[] |
| any | GET /health | errorMsg: "Healthy" |
Request body for POST /ocr#
| Field | Type | Meaning |
|---|---|---|
file required | string | A URL the server can reach, or the Base64 of an image (TIFF included) or PDF. By default only the first 10 pages of a PDF or multi-page TIFF are processed. |
fileType | 0 | 1 | null | 0 = PDF, 1 = image. If omitted, inferred from the URL. |
useDocOrientationClassify, useDocUnwarping, useTextlineOrientation | bool | Turn the preprocessing models on or off for this request |
textDetLimitSideLen, textDetLimitType, textDetThresh, textDetBoxThresh, textDetUnclipRatio, textRecScoreThresh | number / string | Same as the text_det_* / text_rec_* arguments of predict() |
visualize | bool | null | Return result images. If omitted, follows Serving.visualize in the config, and images are returned when neither is set. |
Response#
Success is HTTP 200 with logId, errorCode: 0, errorMsg: "Success" and result. On failure, errorCode equals the HTTP status.2 Here is a real response from our server, trimmed:
{
"logId": "11288c77-b7c3-4060-a177-19ca9adafc42",
"errorCode": 0,
"errorMsg": "Success",
"result": {
"ocrResults": [{
"prunedResult": {
"model_settings": {"use_doc_preprocessor": false, "use_textline_orientation": false},
"dt_polys": [[[243, 167], [604, 120], [608, 151], [247, 198]], "..."],
"text_det_params": {"limit_side_len": 64, "limit_type": "min", "thresh": 0.3,
"max_side_limit": 4000, "box_thresh": 0.6, "unclip_ratio": 1.5},
"rec_texts": ["HARBOR STREET COFFEE", "214 Harbor Street, Portland ME", "..."],
"rec_scores": ["..."],
"rec_polys": ["..."],
"rec_boxes": ["..."]
}
}],
"dataInfo": {"width": 900, "height": 1200, "type": "image"}
}
}prunedResult is the same dict predict() returns in Python, minus input_path and page_index. So rec_texts, rec_scores, rec_polys and rec_boxes mean what they mean in the Python API. With visualization on, each page also gets ocrImage, docPreprocessingImage and inputImage as Base64 JPEGs.2
Those images get big. For one 900×1200 receipt, the default response was 1.23 MB. With "visualize": false it was 4.3 KB, and the three image keys were left out entirely rather than set to null. Read them with .get().4
Errors we triggered#
| Request | Status | errorMsg |
|---|---|---|
No file field | 422 | [{"type": "missing", "loc": "body.file", "msg": "Field required"}] |
file is not valid Base64 | 422 | Invalid input file |
Base64 without fileType | 422 | File type cannot be determined |
Browser CORS preflight (OPTIONS /ocr) | 405 | Method Not Allowed |
Client code: Python, curl, JavaScript#
The Python client below is copied verbatim from the official docs. We ran it unchanged against our server and it printed the 27 receipt lines.24 The curl and JavaScript versions are derived by us from the documented request schema, because the OCR doc has no curl example. We tested both.
import base64
import requests
API_URL = "http://localhost:8080/ocr"
file_path = "./demo.jpg"
with open(file_path, "rb") as file:
file_bytes = file.read()
file_data = base64.b64encode(file_bytes).decode("ascii")
payload = {"file": file_data, "fileType": 1}
response = requests.post(API_URL, json=payload)
assert response.status_code == 200
result = response.json()["result"]
for i, res in enumerate(result["ocrResults"]):
print(res["prunedResult"])
ocr_img_path = f"ocr_{i}.jpg"
with open(ocr_img_path, "wb") as f:
f.write(base64.b64decode(res["ocrImage"]))
print(f"Output image saved at {ocr_img_path}")# GNU base64; -w0 disables line wrapping. Writing to a file avoids
# "argument list too long" on big images.
printf '{"file":"%s","fileType":1,"visualize":false}' "$(base64 -w0 demo.jpg)" > req.json
curl -s -X POST http://localhost:8080/ocr \
-H "Content-Type: application/json" \
-d @req.json | jq -r '.result.ocrResults[].prunedResult.rec_texts[]'
# Or pass a URL the server can reach (fileType is inferred from it):
curl -s -X POST http://localhost:8080/ocr -H "Content-Type: application/json" \
-d '{"file":"https://example.com/invoice.pdf","visualize":false}'// Node.js 18+ (global fetch). Tested on Node 22.
import { readFile } from "node:fs/promises";
const API_URL = "http://localhost:8080/ocr";
const bytes = await readFile("./demo.jpg");
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file: bytes.toString("base64"),
fileType: 1, // 0 = PDF, 1 = image; required for Base64 input
visualize: false // skip the Base64 result images
})
});
const body = await res.json();
if (body.errorCode !== 0) throw new Error(`${body.errorCode}: ${body.errorMsg}`);
for (const page of body.result.ocrResults) {
const { rec_texts, rec_scores } = page.prunedResult;
rec_texts.forEach((t, i) => console.log(rec_scores[i].toFixed(3), t));
}Don't copy the official Node.js example for OCR. In the OCR pipeline doc it posts to http://localhost:8080/layout-parsing and reads layoutParsingResults. Those belong to PP-StructureV3 and PaddleOCR-VL. Against an OCR server that path doesn't exist. Use /ocr and ocrResults as above.2
Calling from a web page: the server sends no CORS headers, and the browser's preflight OPTIONS request gets a 405. Send browser requests through your own backend or a same-origin reverse proxy. If you want OCR in the browser with no server, use PaddleOCR.js, as our online tool does.4
Configure the server: models, speed, page limits#
The default OCR pipeline in PaddleX 3.7.2 uses PP-OCRv6_medium for detection and recognition. It also turns on all three preprocessing models: document orientation (PP-LCNet_x1_0_doc_ori), unwarping (UVDoc) and text-line orientation.3 That is more work than the Python defaults most tutorials show. On our receipt, unwarping gave the same 27 lines but returned them in a different order. To change the defaults, export the config, edit it, and serve the file:5
paddlex --get_pipeline_config OCR --save_path ./
# → "The pipeline config has been saved to: OCR.yaml"This is the edited config we served, as a diff from the export: PP-OCRv6 small models, no preprocessing, no images by default, and no 10-page cap. The Serving keys come from the official docs.12
use_doc_preprocessor: False # was True
use_textline_orientation: False # was True
SubModules:
TextDetection:
model_name: PP-OCRv6_small_det # was PP-OCRv6_medium_det
TextRecognition:
model_name: PP-OCRv6_small_rec # was PP-OCRv6_medium_rec
Serving:
visualize: False # requests can still send "visualize": true
extra:
max_num_input_imgs: null # process every PDF page, not just the first 10paddlex --serve --pipeline ./ocr_config.yaml --device cpu --port 8080With this config the server read all 27 receipt lines. There was one slip: 0rder (with a zero) for "Order", which matches what PP-OCRv6 small does in our benchmark. Which tier to pick is covered on the models page.4
Large responses? Images can be returned as pre-signed URLs instead of Base64 with Serving.return_urls: true. The only storage backend supported is bos (Baidu object storage).1
Docker#
We found no official Docker image for serving the plain OCR pipeline. The official images are for PaddleOCR-VL (below). This Dockerfile is ours. It only uses the documented commands. We built it, ran it, and got the same 27 lines back from /ocr. The image was 2.26 GB on disk.4
FROM python:3.12-slim
# OpenCV (pulled in by the serving plugin) needs these system libraries
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgl1 libglib2.0-0 libgomp1 \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install --no-cache-dir paddlepaddle==3.2.0 paddleocr==3.7.0 \
&& paddlex --install serving -y
COPY ocr_config.yaml /app/ocr_config.yaml
EXPOSE 8080
CMD ["paddlex", "--serve", "--pipeline", "/app/ocr_config.yaml", "--device", "cpu"]docker build -t paddleocr-serve .
# keep downloaded models between restarts
docker run -d -p 8080:8080 -v paddlex-models:/root/.paddlex paddleocr-serve
curl -s localhost:8080/healthModels are not baked in. They download into /root/.paddlex/official_models on first start (from Hugging Face by default), so mount that directory as a volume. For a GPU image, swap the engine for paddlepaddle-gpu from PaddlePaddle's own index, as shown on the install page.
Official images: PaddleOCR-VL#
For PaddleOCR-VL, Baidu publishes images on ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/. They need Docker ≥ 19.03, an NVIDIA GPU and a driver that supports CUDA 12.6+. The full API service is two containers, a VLM server plus the pipeline. You start it with docker compose up using the compose.yaml and .env from deploy/paddleocr_vl_docker/accelerators/nvidia-gpu/, and it listens on port 8080.6
# Must be executed in the directory containing the compose.yaml and .env files
docker compose upThe docs say this setup "can handle only one request at a time". For concurrent requests they point to deploy/paddleocr_vl_docker/hps/.6
High-stability serving#
High-stability serving is built on NVIDIA Triton. The PaddleOCR docs send you to the PaddleX serving guide for it. They also warn that it "may not match the performance of the 2.x version based on PaddleServing". Clients use the same request format as basic serving. Start with basic serving. Move to Triton when you need GPU batching, several instances, or more stability than one Uvicorn process gives you.1
The official hosted API#
The "PaddleOCR official API" is Baidu's hosted service, linked from paddleocr.com, which redirects to AI Studio. In the docs' words, the SDKs "submit local files or file URLs to hosted PaddleOCR services, poll asynchronous jobs, and parse typed results. They do not run local PaddleOCR inference."7
fileUrl, plus model. Returns a jobId.state: pending → running → done or failed.Compare self-hosting: there, one POST /ocr to your own server returns the result directly and nothing leaves your network.
Get the "API key" (access token)#
There is no separate API key product. Sign in to AI Studio and copy the token from the Access Token page. All SDKs and the CLI read it from one environment variable:7
export PADDLEOCR_ACCESS_TOKEN="your-access-token"PADDLEOCR_BASE_URL overrides the service URL, for example to route through your own proxy.7
SDKs and CLI#
| Client | Install | Version (Oct 2026) | Default models |
|---|---|---|---|
Python (PaddleOCRClient, AsyncPaddleOCRClient) | part of paddleocr, no extra | paddleocr 3.7.0 | OCR: PP-OCRv6 Parsing: PaddleOCR-VL-1.6 |
| TypeScript (Node.js 18+) | npm install @paddleocr/api-sdk | 0.2.3 | |
| Go (Go 1.21) | go get github.com/PaddlePaddle/PaddleOCR/api_sdk/go | v0.2.0 | |
| CLI | paddleocr api, ships with paddleocr | 3.7.0 |
from paddleocr import PaddleOCRClient, Model
client = PaddleOCRClient()
result = client.ocr(
file_url="https://example.com/invoice.pdf",
model=Model.PP_OCRV5,
)
print(result.job_id, len(result.pages))
client.close()import { Model, PaddleOCRClient } from "@paddleocr/api-sdk";
const client = new PaddleOCRClient();
const result = await client.ocr({
fileUrl: "https://example.com/invoice.pdf",
model: Model.PPOCRv5,
});
console.log(result.jobId, result.pages.length);client, err := paddleocr.NewClient()
if err != nil {
return err
}
result, err := client.OCR(ctx, &paddleocr.OCRRequest{
Model: paddleocr.PPOCRv5,
FileURL: "https://example.com/invoice.pdf",
})
if err != nil {
return err
}
fmt.Println(result.JobID, len(result.Pages))paddleocr api \
--model_type ocr \
--model PP-OCRv5 \
--file_path ./invoice.pdf \
--request_timeout 300 \
--poll_timeout 600 \
--output ocr-result.jsonThese snippets are verbatim from the official SDK docs.7 Use file_path / filePath / FilePath for a local file, and pass exactly one of URL or path. Each OCR page in the Python result has pruned_result plus image URLs. For PDFs with tables, use parse_document() or --model_type doc_parsing, which return Markdown per page. Errors are typed (AuthError, RateLimitError, PollTimeoutError, …) and all inherit from PaddleOCRAPIError.7
Raw HTTP (derived from the SDK source)#
Calling it from another language? Here is the protocol the Python SDK speaks, read from its source.8 We have not run this ourselves because we didn't use a token for this page. Treat it as a map of the protocol, not a tested recipe.
BASE=https://paddleocr.aistudio-app.com/api/v2/ocr/jobs
# 1. Submit a local file (multipart). JSON with "fileUrl" instead of a file also works.
curl -s -X POST "$BASE" \
-H "Authorization: Bearer $PADDLEOCR_ACCESS_TOKEN" \
-F model=PP-OCRv6 -F 'optionalPayload={}' -F file=@invoice.pdf
# → {"code": 0, "data": {"jobId": "..."}}
# 2. Poll until data.state is "done" (or "failed")
curl -s "$BASE/<jobId>" -H "Authorization: Bearer $PADDLEOCR_ACCESS_TOKEN"
# 3. Download data.resultUrl.jsonUrl: JSONL, one line per pageQuota, limits and the conflicting page caps#
| Limit | Value | Source |
|---|---|---|
| Daily free quota | 3,000 pages per model per day; over that, HTTP 429 | Quota doc9 |
| Pages per file | "Recommended" max 100; beyond that "the API will only parse the first 100 pages" | Quota doc (2026-02-10)9 |
| Pages per request | "Supports up to 1000 PDF pages per request" | Async guide (2026-02-04)10 |
| File size | URLs ≤ 200 MB; uploads ≤ 50 MB | Async guide10 |
| More quota | A questionnaire to "apply for a free quota increase" | Quota doc9 |
| Price | Not stated on any page we could read | — |
100 or 1000 pages? Two official AI Studio pages disagree, and the newer one (the quota doc) says 100. Split large PDFs into chunks of 100 pages or fewer, or use --page_ranges / page ranges in the SDK, until Baidu clarifies. Error codes from the quota doc: 403 invalid token, 413 too large, 422 bad parameter, 429 daily cap, 503 too many requests, 504 gateway timeout.910
Privacy. With the hosted API, every file, or every URL you hand it, is processed on Baidu's servers. Results come back as links to stored files. Don't send documents you aren't allowed to share with a third-party processor. Use self-hosting or in-browser OCR for those. Also note: the AI Studio PP-OCRv5 API page says the hosted service "corresponds to PaddleX version 3.3.12". That is older than the 3.7 stack you would self-host.
Doc mistakes and look-alikes to avoid#
- Wrong endpoint in the OCR Node.js example: it uses
/layout-parsinginstead of/ocr(OCR.en.md:2471–2487).2 - Mislabelled API reference links. The "PP-OCRv5 API" link in the SDK docs opens the PaddleOCR-VL page, and the PP-OCRv5 page is behind the "PP-StructureV3 API" label. Go by the page title, not the link text.12
- npm look-alikes. The unscoped
paddleocrandpaddleocr-jspackages are not from the PaddleOCR team. The official ones are scoped:@paddleocr/api-sdk(hosted API, Node.js) and@paddleocr/paddleocr-js(browser).11 - PaddleHub serving (
deploy/hubserving) is still in the repo from the 2.x era, but the 3.x serving guide recommends PaddleX. We have not checked whether hubserving works with 3.x. If you are upgrading old code, start with the migration guide.
Hooking OCR into an AI agent? The PaddleOCR MCP server can use your self-hosted server as its backend (basic serving only).
Questions#
Is there a free PaddleOCR API?
Yes, two kinds. Self-hosting with paddlex --serve is free and open source (Apache-2.0). You only pay for hardware. The hosted official API has a free quota of 3,000 pages per model per day, according to Baidu's quota doc. We found no published price beyond that.
Where do I get a PaddleOCR API key?
The hosted API uses an AI Studio access token, from aistudio.baidu.com/account/accessToken. Export it as PADDLEOCR_ACCESS_TOKEN. The self-hosted server has no key at all, so add authentication in a reverse proxy if it's reachable from outside.
Why does my request return 422 "File type cannot be determined"?
You sent Base64 without fileType. Add "fileType": 1 for images or 0 for PDFs. Only URLs can be type-sniffed.
Why does the server only read the first 10 pages of my PDF?
Basic serving caps PDFs and multi-page TIFFs at 10 pages by default. Add Serving: extra: max_num_input_imgs: null to your pipeline config and serve that file.
Do I need to write a FastAPI wrapper around PaddleOCR?
Usually not. PaddleX basic serving already is a FastAPI app on Uvicorn, with /health, /docs and a documented JSON schema. Write your own only if you need custom routes or auth inside the same process.
Can I call the self-hosted server from browser JavaScript?
Not directly from another origin. It sends no CORS headers, and the preflight gets a 405. Route the call through your backend or a same-origin proxy, or run OCR in the browser with PaddleOCR.js.
Is there an official PaddleOCR Docker image for the API?
Only for PaddleOCR-VL (GPU, CUDA 12.6+). For the plain OCR pipeline we found none documented. The Dockerfile on this page is our own, and we built and tested it.
Sources
- Serving guide (basic serving, CLI options, high-stability serving, URL return): docs/version3.x/inference_deployment/serving/serving.en.md, lines 7–118.
- General OCR pipeline, section 3 "Development Integration/Deployment" (
POST /ocr, request and response schema, Python client at lines 2144–2165, Node.js example at lines 2471–2487): docs/version3.x/pipeline_usage/OCR.en.md. - PaddleX 3.7.2 source (installed with PaddleOCR 3.7.0): basic serving runs FastAPI on Uvicorn (
paddlex/inference/serving/basic_serving/_server.py), exposesGET /health(_app.py:205); default OCR pipeline configpaddlex/configs/pipelines/OCR.yaml; serving extra dependencies from the wheel METADATA. - Our test, 2026-10-27/28: Linux x86-64, 4 vCPU, Python 3.13,
paddlepaddle==3.2.0,paddleocr==3.7.0,paddlex==3.7.2;paddlex --install serving,paddlex --serve --pipeline OCR, requests sent with the official Python client, curl and Node.js 22fetch; Docker image built frompython:3.12-slim. The machine was shared with other jobs, so we do not publish server timings. - PaddleOCR and PaddleX (pipeline registration names,
paddlex --get_pipeline_config): docs/version3.x/paddleocr_and_paddlex.en.md, lines 41–80. Layout parsing endpointPOST /layout-parsing: PP-StructureV3.en.md:3082 and PaddleOCR-VL.en.md:2290. - PaddleOCR-VL service deployment with Docker Compose, images and requirements: docs/version3.x/pipeline_usage/PaddleOCR-VL.en.md, lines 274–309 and 2004–2050.
- Official API SDK docs: overview, Python, TypeScript, Go and CLI pages (paddleocr_official_api/*.en.md).
- Official API client source:
paddleocr/_api_client/_http.py(base URLhttps://paddleocr.aistudio-app.com, path/api/v2/ocr/jobs, Bearer header, JSON and multipart submit) and_core.py(job states,jobId,resultUrl.jsonUrl). - AI Studio, "API Quota Rules and Error Code Description" (updated 2026-02-10): ai.baidu.com/ai-doc/AISTUDIO/pmjcld5qm, read 2026-10-27.
- AI Studio, "Asynchronous API Usage Guide" (updated 2026-02-04): ai.baidu.com/ai-doc/AISTUDIO/Mml7n69e7, read 2026-10-27.
- npm registry, 2026-10-27: @paddleocr/api-sdk 0.2.3 (published 2026-06-11); search results for "paddleocr" showing unscoped look-alike packages. Go proxy:
api_sdk/gov0.2.0. - AI Studio page titles behind the "Official API Reference" links, read 2026-10-27: Dmh4onssk = PaddleOCR-VL_API_en, 7mfz6dgx9 = PP-OCRv5_API_en, Vmkz2nz1p = PaddleOCR-VL-1.5_API_en, fml7mozw5 = the Chinese async API guide. The repo labels them differently (python.en.md:122–125).