Papermerge 3.5.3 API 使用详解
Papermerge 3.5.3 REST API 使用详解:token 认证、节点/上传/OCR/标签/检索/用户各域实测命令与原始回显、分页错误约定与十一个 API 坑
Papermerge 3.5.3 API 使用详解
目标:把 Papermerge 3.5.3 的 REST API 从头用到尾——拿 token、建目录/文档、上传、OCR、打标签、挂自定义字段、全文检索、用户管理,每个端点给出实测命令与原始回显,并沉淀 3.x API 的十余个行为坑。
本文实测环境为《Papermerge 3.5.3 Docker Compose 部署指南》部署的六服务栈(Web 入口 :8138);配置参数见《Papermerge 3.5.3 配置参数详解》;组件定位见《Papermerge 介绍与选型指南》。所有命令与回显均为 2026-11-02 实测照抄,示例账号 admin / q1w2e3r4。
为什么写 API 篇而不是 CLI 篇:3.x 已移除 2.x 的
papermanage全家桶,镜像内也没有可用的管理 CLI(官方 token 文档推荐的create_token.sh实测已损坏,见第七章问题 3),REST API 是 3.5.3 唯一的编程入口。
一、环境信息
| 项目 | 值 |
|---|---|
| 实例版本 | Papermerge 3.5.3(GET /api/version/ 实测回显) |
| API 基址 | http://127.0.0.1:8138/api(生产替换为服务器地址) |
| 路径风格 | 无版本段:/api/token、/api/nodes/……没有 /v1(见问题 1) |
| 认证方式 | Authorization: Bearer <JWT>(由 POST /api/token 登录换取) |
| OpenAPI 规范 | GET /openapi.json 匿名可读,title "Papermerge DMS REST API" version 3.5.3,53 个路径 |
| Swagger UI | GET /api/docs(需 token,匿名 401) |
| 测试凭据 | admin / q1w2e3r4(超级用户);quzuo / q1w2e3r4(普通用户,本文创建) |
| 测试文件 | invoice-scan.pdf(81232 字节,德文扫描件,含词 KAFFEEMUEHLE)、notes-text.pdf(631 字节,文本层含 778811) |
二、前置条件
- 六服务栈已部署且
GET /api/probe/返回 200(部署与验证见《Docker Compose 部署指南》)。 - 全文检索类端点要拿到命中结果,Solr core 必须已打 schema 补丁(copyField
*_txt_en → _text_)——官方默认 configset 下检索恒 0 命中,修复过程见部署指南 4.1 与第九章问题 4。 - 备好 curl 与任一 JSON 解析工具(本文用 python3 提取字段)。
- 下文用
$TOKEN表示 4.1 拿到的 access_token。
三、API 总览
3.1 规范与文档入口
实例自带 OpenAPI 规范且匿名可读(实测):
curl -s http://127.0.0.1:8138/openapi.json | python3 -c \
"import json,sys;d=json.load(sys.stdin);print(d['info']['title'],d['info']['version'],len(d['paths']))"
Papermerge DMS REST API 3.5.3 53
交互式 swagger 在 /api/docs,但走 nginx 的 auth_request,匿名 401、带 token 200(实测):
curl -s -o /dev/null -w 'docs HTTP=%{http_code}\n' http://127.0.0.1:8138/api/docs
curl -s -o /dev/null -w 'docs HTTP=%{http_code}\n' http://127.0.0.1:8138/api/docs -H "Authorization: Bearer $TOKEN"
docs HTTP=401
docs HTTP=200
UI 侧入口为 User Menu → REST API(官方文档 rest-api/reference,查询日期 2026-11-01),背后就是这个 /api/docs。
3.2 网关路由(为什么路径是 /api/*)
webapp 容器内 nginx 实测配置(/etc/nginx/nginx.conf 节选):
location ~ ^/api(/?)(.*) {
auth_request /verify;
proxy_pass http://127.0.0.1:8000/$2$is_args$args;
}
location /docs { proxy_pass http://127.0.0.1:8000/docs; }
location /openapi.json { proxy_pass http://127.0.0.1:8000/openapi.json; }
location = /verify { proxy_pass http://127.0.0.1:4010; }
即:/api/xxx 被反代为 Django 侧的 /xxx(/api 前缀在转发时被剥掉),每个 /api 请求先经内置 auth-server(:4010) 的 /verify 校验 Bearer token。这就是"路径没有 /v1"(openapi 里就没有)与"匿名一律 401"的机制根源。
3.3 端点域一览(53 路径按域归组)
| 域 | 端点(省略 /api 前缀) | 本文实测 |
|---|---|---|
| 认证 | POST /token(auth-server 直供) | ✅ |
| 元信息 | GET /version/、GET /probe/、GET /docs、GET /openapi.json | ✅ |
| 节点 | POST/GET/DELETE /nodes/、GET /nodes/{parent_id}、PATCH /nodes/{node_id}、POST /nodes/move、GET /folders/{folder_id} | ✅ |
| 文档 | GET /documents/{id}、POST /documents/{id}/upload、GET /documents/{id}/versions、GET /documents/{id}/last-version/、PATCH /documents/{id}/type、GET+PATCH /documents/{id}/custom-fields、GET /documents/type/{document_type_id} | ✅(type/{id} 列表未深测) |
| 版本 | GET /document-versions/{id}、GET+HEAD /document-versions/{id}/download、GET /document-versions/{id}/download-url | ✅ |
| 缩略图 | GET /thumbnails/{document_id}、GET /documents/thumbnail-img-status/ | ✅(后者列表未深测) |
| OCR 任务 | POST /tasks/ocr | ✅ |
| 检索 | GET /search/ | ✅ |
| 标签 | GET+POST /tags/、GET /tags/all、GET+PATCH+DELETE /tags/{tag_id}、GET+POST+PATCH+DELETE /nodes/{node_id}/tags | ✅ |
| 文档类型 | GET+POST /document-types/、GET /document-types/all、GET+PATCH+DELETE /document-types/{id} | ✅ |
| 自定义字段 | GET+POST /custom-fields/、GET /custom-fields/all、GET+PATCH+DELETE /custom-fields/{id} | ✅ |
| 用户 | GET /users/me、GET+POST /users/、GET /users/all、GET+PATCH+DELETE /users/{id}、POST /users/{id}/change-password、GET /users/group-homes、GET /users/group-inboxes | ✅(change-password 等列表未深测) |
| 组/角色 | GET+POST /groups/、/groups/all、/groups/{id};/roles/ 同构 | ✅(空列表) |
| 页面 | POST /pages/、POST /pages/move、POST /pages/extract | ❌ 未实测 |
| 共享 | GET+POST /shared-nodes/、GET+PATCH /shared-nodes/access/{node_id}、GET /shared-nodes/folder/{parent_id}、GET /shared-documents/{id}、GET /shared-folders/{id} | ❌ 未实测 |
四、认证与首次调用
4.1 拿 token:POST /api/token(必须 JSON)
curl -s -X POST http://127.0.0.1:8138/api/token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}'
实测回显(access_token 截前 260 字符,全文 729 字符):
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMGRiYTI5NC1hOTRmLTQ0NzktODhkZS1jOWRmMGFjNGMxN2YiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJzY29wZXMiOlsibm9kZS5jcmVhdGUiLCJub2RlLnZpZXciLCJub2RlLnVwZGF0ZSIsIm
TOKEN_LEN=729
两个实测约束:编码必须 JSON(form 编码 422,见问题 2);响应同时下发 set-cookie: access_token=...(浏览器 UI 会话即此通道,Cookie 名由 SECURITY__COOKIE_NAME 配置,默认 access_token,见《Papermerge 3.5.3 配置参数详解》5.5)。
官方文档说 token 用镜像内 create_token.sh 生成——3.5.3 实测该脚本已损坏(could not find task "auth-cli"),登录换 JWT 是唯一可用路线(问题 3)。
4.2 JWT 里有什么
对 4.1 的 token 做 base64 解码 payload(实测回显):
{"sub": "a0dba294-a94f-4479-88de-c9df0ac4c17f", "preferred_username": "admin", "email": "admin@example.com", "scopes": ["node.create", "node.view", "node.update", "node.delete", "node.move", "document.upload", "document.download", "task.ocr", "tag.create", "tag.view", "tag.update", "tag.delete", "user.me", "user.create", "user.view", "user.update", "user.delete", "group.create", "group.view", "group.update", "group.delete", "ocrlang.view", "page.view", "page.update", "page.move", "page.extract"], "exp": 1793612454}
sub是用户 UUID(不是用户名);scopes决定能调哪些端点;- 有效期:实测新 token 的
exp距签发时刻剩余秒 = 21599 ≈ 359 分钟,与默认配置SECURITY__TOKEN_EXPIRE_MINUTES=360吻合; - token 长度随 scopes 变化:超级用户 729 字符,无权限的普通用户仅 257 字符(见 5.7 与问题 9)。
4.3 带 token 与不带 token
curl -s -o /dev/null -w 'HTTP=%{http_code}\n' http://127.0.0.1:8138/api/users/me
curl -s http://127.0.0.1:8138/api/users/me -H "Authorization: Bearer $TOKEN"
HTTP=401
Unauthorized
401 由 nginx auth_request 拦截产生(响应体是纯文本 Unauthorized,不是 JSON);通过网关后业务层的拒绝则是 JSON {"detail":"Access Forbidden"}(403,见 5.7)。
4.4 首次调用三连
curl -s http://127.0.0.1:8138/api/version/ -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w 'HTTP=%{http_code} bytes=%{size_download}\n' http://127.0.0.1:8138/api/probe/ -H "Authorization: Bearer $TOKEN"
curl -s http://127.0.0.1:8138/api/users/me -H "Authorization: Bearer $TOKEN"
实测回显:
{"version":"3.5.3"}
HTTP=200 bytes=0
{"id":"a0dba294-a94f-4479-88de-c9df0ac4c17f","username":"admin","email":"admin@example.com","created_at":"2026-11-01T16:07:15.292229","updated_at":"2026-11-01T16:07:15.292229","home_folder_id":"6a1bdab1-a5ca-4475-be7e-627631ba8a02","inbox_folder_id":"a6e59095-e270-4f9a-9493-92f8a723d291","is_superuser":true,"is_active":true,"scopes":["custom_field.create","custom_field.delete", ...全量清单... ]}
users/me 的 inbox_folder_id 是后续所有建节点操作的根——Papermerge 每个用户有 home/inbox 两个隐式根目录,新建 folder/document 的 parent_id 从这里取(见 5.1)。
五、各 API 域详解
以下命令均在 export TOKEN=<4.1 所得> 后实测;为省版面,http://127.0.0.1:8138 以 $BASE 代指。
5.1 nodes:目录与文档节点
建目录(ctype=folder,parent_id 必填):
curl -s -X POST $BASE/api/nodes/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"finance","ctype":"folder","parent_id":"a6e59095-e270-4f9a-9493-92f8a723d291"}'
实测回显(提取 id):
FID=48f098ee-8f86-4a24-ae84-bd11d4efdb8c
建文档节点(ctype=document,可带 lang 与 ocr 标志):
curl -s -X POST $BASE/api/nodes/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"pm-invoice","ctype":"document","lang":"deu","ocr":false,"parent_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c"}'
DOCID3=a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd
不带 parent_id 实测 422(问题 4):
{"detail":[{"type":"missing","loc":["body","NewDocument","parent_id"],"msg":"Field required",...}]}
列目录内容(GET /api/nodes/{parent_id},分页信封):
curl -s $BASE/api/nodes/48f098ee-8f86-4a24-ae84-bd11d4efdb8c -H "Authorization: Bearer $TOKEN"
实测回显(节选,finance 目录下两个文档):
{"page_size":5,"page_number":1,"num_pages":1,"items":[{"id":"d39190b7-45b6-420e-ba87-465eff974cd4","title":"pm-d5","ctype":"document","tags":[],"parent_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c","document_type_id":null,"breadcrumb":[],"ocr":false,"ocr_status":"UNKNOWN","thumbnail_url":"/api/thumbnails/d39190b7-45b6-420e-ba87-465eff974cd4","preview_status":null,"user_id":"a0dba294-a94f-4479-88de-c9df0ac4c17f","group_id":null,"owner_name":null,"perms":[],"is_shared":false}, ...]}
注意:裸 GET /api/nodes/ 实测返回空数组 [](HTTP 200)——它不是"全局节点列表",列目录必须带父节点 id。
节点详情(GET /api/documents/{id},含面包屑):
curl -s $BASE/api/documents/b55c1248-fb18-4f75-b41f-befbbc9e6243 -H "Authorization: Bearer $TOKEN"
实测回显(节选,pm-notes 移动后的面包屑):
{"id":"b55c1248-...","title":"pm-notes","ctype":"document",...,"breadcrumb":[["a6e59095-e270-4f9a-9493-92f8a723d291","inbox"],["48f098ee-8f86-4a24-ae84-bd11d4efdb8c","finance"],["b55c1248-fb18-4f75-b41f-befbbc9e6243","pm-notes"]],"ocr":true,"ocr_status":"UNKNOWN",...}
改名(PATCH /api/nodes/{node_id}):
curl -s -X PATCH $BASE/api/nodes/b55c1248-fb18-4f75-b41f-befbbc9e6243 \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"pm-notes-renamed"}'
实测回显(PATCH 响应是精简节点对象,字段集与 GET 详情不同——以回显为准):
HTTP=200
{"id":"b55c1248-fb18-4f75-b41f-befbbc9e6243","title":"pm-notes-renamed","ctype":"document","tags":[],"created_at":"2026-11-02T00:49:54.614163","updated_at":"2026-11-02T04:57:32.244495","parent_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c","user_id":"a0dba294-a94f-4479-88de-c9df0ac4c17f","document":null}
移动(POST /api/nodes/move,body 为 source_ids 数组 + target_id):
curl -s -X POST $BASE/api/nodes/move -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"source_ids":["b55c1248-fb18-4f75-b41f-befbbc9e6243"],"target_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c"}'
HTTP=200
["b55c1248-fb18-4f75-b41f-befbbc9e6243"]
移动结果用上面的面包屑回显验证(pm-notes 的 parent 变为 finance)。
删除节点(DELETE /api/nodes/,body 必须是裸 UUID 数组):
curl -s -X DELETE $BASE/api/nodes/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '["d39190b7-45b6-420e-ba87-465eff974cd4"]'
HTTP=200
null
包成对象 {"node_ids":[...]} 实测 422(问题 5):
{"detail":[{"type":"list_type","loc":["body"],"msg":"Input should be a valid list","input":{"node_ids":["d39190b7-45b6-420e-ba87-465eff974cd4"]}}]}
5.2 documents:上传、版本与下载
上传(POST /api/documents/{id}/upload,multipart)。文档节点必须先建(5.1),上传只是给节点灌文件:
curl -s -X POST $BASE/api/documents/d39190b7-45b6-420e-ba87-465eff974cd4/upload \
-H "Authorization: Bearer $TOKEN" -F "file=@invoice-scan.pdf"
实测回显(完整文档对象原文照抄):
{"id":"d39190b7-45b6-420e-ba87-465eff974cd4","title":"pm-d5","ctype":"document","tags":[],"parent_id":"48f098ee-8f86-4a24-ae84-bd11d4efdb8c","document_type_id":null,"breadcrumb":[],"ocr":false,"ocr_status":"UNKNOWN","thumbnail_url":"/api/thumbnails/d39190b7-45b6-420e-ba87-465eff974cd4","preview_status":null,"user_id":"a0dba294-a94f-4479-88de-c9df0ac4c17f","group_id":null,"owner_name":"admin","perms":[],"is_shared":false,"versions":[{"id":"6ed32054-c4bf-4c5d-9aea-aa1090653ea0","number":1,"lang":"deu","file_name":"invoice-scan.pdf","size":81232,"page_count":1,"short_description":"Original","document_id":"d39190b7-45b6-420e-ba87-465eff974cd4","download_url":"/api/document-versions/6ed32054-c4bf-4c5d-9aea-aa1090653ea0/download","pages":[{"id":"3f5d67e0-f046-41f9-9bc0-6ee35674b3d6","number":1}]}]}
要点:响应即最新版本快照(versions[0] 含 size/page_count/pages/download_url);lang 取文档节点的 lang;若建节点时 ocr=true,上传即自动调度 OCR(5.3)。
版本列表与最新版本:
curl -s $BASE/api/documents/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd/versions -H "Authorization: Bearer $TOKEN"
curl -s $BASE/api/documents/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd/last-version/ -H "Authorization: Bearer $TOKEN"
实测回显:
[{"id":"665be4ac-5fd7-48aa-80c8-94d390b36e89","number":2,"short_description":"With OCR text layer"},{"id":"7371b12f-0f45-48e5-9e9b-2da9fa490d68","number":1,"short_description":"Original"}]
{"id":"665be4ac-5fd7-48aa-80c8-94d390b36e89","number":2,"lang":"deu","file_name":"invoice-scan.pdf","size":0,"page_count":1,"short_description":"With OCR text layer","document_id":"a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd","download_url":"/api/document-versions/665be4ac-5fd7-48aa-80c8-94d390b36e89/download","pages":[{"id":"7a30ce83-ae00-4bc6-b638-27387140c421","number":1}]}
OCR 完成后自动产生 number:2 的 "With OCR text layer" 版本(原始版本保留,可回溯)。注意 last-version 里 OCR 版 size:0 是显示怪癖,实际文件 50126 字节(见下面下载)。
下载 URL 与实际下载:
curl -s $BASE/api/document-versions/665be4ac-5fd7-48aa-80c8-94d390b36e89/download-url -H "Authorization: Bearer $TOKEN"
curl -s $BASE/api/document-versions/665be4ac-5fd7-48aa-80c8-94d390b36e89/download -H "Authorization: Bearer $TOKEN" -o /tmp/pm-dl.pdf
{"downloadURL":"/api/document-versions/665be4ac-5fd7-48aa-80c8-94d390b36e89/download"}
downloaded version=665be4ac-5fd7-48aa-80c8-94d390b36e89 bytes=50126 sha256=b400cab5c5160e3f
download-url 返回的是站内相对路径(FILE_SERVER=s3 形态下才是 CloudFront 签名外链,见《Papermerge 3.5.3 配置参数详解》5.6);GET /download 直接回文件字节流。
缩略图:
curl -s -o /tmp/thumb.jpg -w 'HTTP=%{http_code} bytes=%{size_download} type=%{content_type}\n' \
$BASE/api/thumbnails/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd -H "Authorization: Bearer $TOKEN"
HTTP=200 bytes=5147 type=image/jpeg
尺寸由 PREVIEW__PAGE_SIZE_SM 控制(默认 200px)。
挂文档类型与自定义字段值:
curl -s -X PATCH $BASE/api/documents/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd/type \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"document_type_id":"d94c424d-e74e-4aa0-8b77-4d95171145af"}'
curl -s -X PATCH $BASE/api/documents/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd/custom-fields \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '[{"key":"InvoiceNr","value":"PM-2026-0001"}]'
curl -s $BASE/api/documents/a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd/custom-fields -H "Authorization: Bearer $TOKEN"
实测回显(依次为:设类型 200 空体;赋值 200;回读带值):
HTTP=200
null
HTTP=200
[{"document_id":"a760b7a0-...","document_type_id":"d94c424d-...","custom_field_id":"061e84fb-...","name":"InvoiceNr","type":"text","extra_data":null,"custom_field_value_id":null,"value":null}]
[{"document_id":"a760b7a0-...","document_type_id":"d94c424d-...","custom_field_id":"061e84fb-...","name":"InvoiceNr","type":"text","extra_data":null,"custom_field_value_id":"b041afe3-02d1-4197-bf9a-1b8fdd0082a5","value":"PM-2026-0001"}]
顺序敏感:必须先给文档设类型,再 PATCH 字段值——类型未设时赋值回读 [](实测)。custom-fields PATCH 的 body 是 {"key":字段名,"value":值} 的裸数组。
5.3 OCR 任务
手动调度(POST /api/tasks/ocr,body 为 document_id + lang):
curl -s -X POST $BASE/api/tasks/ocr -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"document_id":"a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd","lang":"deu"}'
null
HTTP 200 + 空体 null=已投递 ocr 队列,异步执行;单页 PDF 从投递到文本入库实测约 1-2 秒。建节点时 "ocr":true 则上传即自动调度(实测 pm-notes 上传后 ocr=True)。
lang 参数校验:传非白名单码实测 422 并回显全部合法码(24 个):
{"detail":[{"type":"literal_error","loc":["body","lang"],"msg":"Input should be 'ces', 'dan', 'deu', 'ell', 'eng', 'fas', 'fin', 'fra', 'guj', 'heb', 'hin', 'ita', 'jpn', 'kor', 'lit', 'nld', 'nor', 'pol', 'por', 'ron', 'san', 'spa', 'kaz' or 'rus'","input":"zzz","ctx":{"expected":"'ces', 'dan', 'deu', 'ell', 'eng', 'fas', 'fin', 'fra', 'guj', 'heb', 'hin', 'ita', 'jpn', 'kor', 'lit', 'nld', 'nor', 'pol', 'por', 'ron', 'san', 'spa', 'kaz' or 'rus'"}}]}
白名单 ∩ 镜像 tesseract 语言包(实测仅 deu/eng/osd/ron/spa)才是真正可用集;白名单内缺包(如 jpn)照样受理返回 null,运行时才失败(问题 10)。
没有任务状态端点:openapi 里 /tasks/ 仅 POST /tasks/ocr 一个路径。验证 OCR 是否真跑完,实测看三处——Redis 的 celery-task-meta-*(全 SUCCESS)、media 盘 ocr/pages/.../page.txt 产物、以及 GET /api/documents/{id} 的 ocr 布尔位(注意 ocr_status 实测恒为 UNKNOWN,不可依赖)。
5.4 tags:标签
建标签(POST /api/tags/,颜色可自定义):
curl -s -X POST $BASE/api/tags/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"invoice-2026","bg_color":"#FF0000","fg_color":"#FFFFFF"}'
{"id":"f58b3abc-97e0-4c49-a03a-631c7bd097fd","name":"invoice-2026","bg_color":"#FF0000","fg_color":"#FFFFFF","description":null,"pinned":false,"group_id":null,"group_name":null}
列表:GET /api/tags/ 分页信封(默认 page_size=5),GET /api/tags/all 裸数组(实测回显为后来自动重建的默认色标签):
{"page_size":5,"page_number":1,"num_pages":1,"items":[{"id":"373b1c53-a136-4f0f-a708-1578adb40e2c","name":"invoice-2026","bg_color":"#c41fff","fg_color":"#FFFFF","description":null,"pinned":false,"group_id":null,"group_name":null}]}
[{"id":"373b1c53-a136-4f0f-a708-1578adb40e2c","name":"invoice-2026","bg_color":"#c41fff","fg_color":"#FFFFF","description":null,"pinned":false,"group_id":null,"group_name":null}]
给节点打标签(PATCH /api/nodes/{node_id}/tags)——本文最大的 API 坑:openapi schema 是 array[string],但字符串按"标签名"解释,不是标签 UUID(问题 6)。实测行为链:
- 传
["<tag_uuid>"]→ 按名找不到就新建一个名字是那串 UUID 的标签并挂上(实测节点 tags 出现{"name":"f58b3abc-97e0-...","bg_color":"#c41fff"}); - 传已存在的名字
["invoice-2026"]→ 400 直接把数据库唯一约束错误抛给客户端:
{"detail":{"attrs":null,"messages":["(sqlalchemy.dialects.postgresql.asyncpg.IntegrityError) <class 'asyncpg.exceptions.UniqueViolationError'>: duplicate key value violates unique constraint \"unique tag name per user\"..."]}}HTTP=400
- 传不存在的名字 → 自动建默认色标签(bg
#c41fff)并挂载;实测 HTTP 500 但副作用生效(响应码不可信,以回读为准)。
正确姿势:先 POST /api/tags/ 建好标签,再 PATCH /api/nodes/{id}/tags -d '["invoice-2026"]'——名字在全用户唯一(unique tag name per user),回读节点详情 tags 数组确认。删除标签 DELETE /api/tags/{tag_id} 实测 204。
5.5 document-types 与 custom-fields
建自定义字段(POST /api/custom-fields/,type 支持 text/number/date 等):
curl -s -X POST $BASE/api/custom-fields/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"InvoiceNr","type":"text"}'
{"id":"061e84fb-1c2b-4aa7-8be4-cec63b08fab4","name":"InvoiceNr","type":"text","extra_data":null,"group_id":null,"group_name":null}
建文档类型(POST /api/document-types/)——name 与 custom_field_ids 都是必填(问题 7),实测缺字段的 422:
{"detail":[{"type":"missing","loc":["body","name"],"msg":"Field required","input":{"label":"Rechnung","path":"finance/"}},{"type":"missing","loc":["body","custom_field_ids"],"msg":"Field required","input":{"label":"Rechnung","path":"finance/"}}]}
(顺带可见:文档类型 JSON 里用 label/path 是 2.x 时代字段,3.5.3 不认。)正确创建:
curl -s -X POST $BASE/api/document-types/ -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Rechnung5","custom_field_ids":["061e84fb-1c2b-4aa7-8be4-cec63b08fab4"]}'
{"id":"d94c424d-e74e-4aa0-8b77-4d95171145af","name":"Rechnung5","path_template":null,"custom_fields":[{"id":"061e84fb-1c2b-4aa7-8be4-cec63b08fab4","name":"InvoiceNr","type":"text","extra_data":null,"group_id":null,"group_name":null}],"group_id":null,"group_name":null}
列表:GET /api/document-types/(分页信封)与 /all(裸数组)、GET /api/custom-fields/ 与 /all 同 5.4 规律,实测回显见上两例结构。类型与字段的单条 GET/PATCH/DELETE /{id} 端点存在(openapi 53 路径),本文未逐一展开。
5.6 search:全文检索
curl -s "$BASE/api/search/?q=KAFFEEMUEHLE" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/api/search/?q=778811" -H "Authorization: Bearer $TOKEN"
实测回显(最终修复态):
{"page_size":10,"page_number":1,"num_pages":1,"items":[{"id":"88a68182-b667-4174-ac68-2e49c0f5847e","title":"invoice-scan","lang":"en","tags":[],"page_number":1,"document_id":"125122b8-baca-4533-a47b-e64bafa48204","entity_type":"document"},{"id":"9e70675f-4504-4c8b-84ff-f78bc87bd08d","title":"invoice-fresh","lang":"en","tags":[],"page_number":1,"document_id":"2a4692ef-d728-4c2d-924d-d8e23b91d305","entity_type":"document"},{"id":"7a30ce83-ae00-4bc6-b638-27387140c421","title":"pm-invoice","lang":"en","tags":[],"page_number":1,"document_id":"a760b7a0-8e08-4023-9f5a-7b2f7f6eeccd","entity_type":"document"}]}
{"page_size":10,"page_number":1,"num_pages":1,"items":[{"id":"3a6508af-1199-4100-8154-3f9ad27b7f23","title":"notes-text","lang":"en","tags":[],"page_number":1,"document_id":"7b3e8499-fcdc-4e33-bd81-84d2a4ed3843","entity_type":"document"},{"id":"84585308-cead-4240-846b-fa97c800148b","title":"pm-notes","lang":"en","tags":[],"page_number":1,"document_id":"b55c1248-fb18-4f75-b41f-befbbc9e6243","entity_type":"document"}]}
解读(全部实测):
q=KAFFEEMUEHLE(纯扫描件 OCR 出的词)命中 3 条、q=778811(PDF 自带文本层)命中 2 条——OCR 文本与原生文本层同权入索引;q=invoice还能命中标题(title 也 copyField 进_text_)。- 命中项是页粒度:
id是页 UUID,document_id才是文档;page_number为命中页码;entity_type实测均为document。 - 结果自动按用户隔离:索引文档带
user_id字段,webapp 发出的查询是q=<词> AND user_id:<当前用户>(Solr 访问日志实测可见),无需也无法手动传用户。 - 分页:
page_size(默认 10,openapi schema)与page_number(默认 1),实测page_size=1两页:
{"page_size":1,"page_number":1,"num_pages":4,"items":[{"id":"88a68182-...","title":"invoice-scan", ...}]}
{"page_size":1,"page_number":2,"num_pages":4,"items":[{"id":"9e70675f-...","title":"invoice-fresh", ...}]}
- 检索 0 命中先查索引层而不是 API:官方默认 Solr configset 缺 copyField 时本端点恒空(部署指南第九章问题 4/6/7)。
5.7 users / groups / roles
建用户(POST /api/users/)——role_ids 与 group_ids 必填(可空数组,问题 8),实测缺字段的 422:
{"detail":[{"type":"missing","loc":["body","role_ids"],"msg":"Field required","input":{"username":"quzuo","email":"quzuo@example.com","password":"q1w2e3r4","is_superuser":false,"is_active":true}},{"type":"missing","loc":["body","group_ids"],"msg":"Field required","input":{"username":"quzuo","email":"quzuo@example.com","password":"q1w2e3r4","is_superuser":false,"is_active":true}}]}
补齐后创建成功(实测回显,新用户自动获得自己的 home/inbox 根目录):
{"id":"1aee9e47-8173-4c80-a688-3351c4197eac","username":"quzuo","email":"quzuo@example.com","created_at":"2026-11-02T01:20:58.986077","updated_at":"2026-11-02T01:20:58.986077","home_folder_id":"9662aba9-71f9-46b0-bedf-2f11bf455b13","inbox_folder_id":"c9ec56d7-3e04-4ff9-8d69-d9c49bb3954e","is_superuser":false,"is_active":true,"scopes":[]}
列表:GET /api/users/ 分页信封(admin 实测见 2 用户);GET /api/users/all 裸数组。GET /api/users/me 见 4.4。
权限实测:上面这样"裸建"的用户 scopes 为空——用它登录拿到的 token 只有 257 字符,调 GET /api/users/ 与 GET /api/users/me 都返回 403:
users HTTP=403
{"detail":"Access Forbidden"}
即 3.5.3 里"建用户 ≠ 授权":权限要走 roles/groups(POST /api/roles/、POST /api/groups/ 及用户体的 role_ids/group_ids)赋予;本实例未建任何角色/组,实测 GET /api/roles/、GET /api/groups/ 均为 {"page_size":5,"page_number":1,"num_pages":0,"items":[]}。POST /api/users/{id}/change-password、GET /api/users/group-homes|group-inboxes 端点在 openapi 中存在,本文未实测。
六、分页 / 过滤 / 排序通用约定
实测归纳的 API 通用规律:
- 两种响应形态:列表端点默认返回分页信封
{"page_size":N,"page_number":N,"num_pages":N,"items":[...]};带/all后缀的端点(tags/all、document-types/all、custom-fields/all、users/all、groups/all、roles/all)返回裸数组、无分页。列表默认page_size=5(实测各列表回显),search 默认page_size=10(openapi schema)。 - 例外:
GET /api/nodes/(不带父 id)实测返回裸[],不是信封也不是全量列表——列内容请用GET /api/nodes/{parent_id}。 - 主键一律 UUID;路径参数名不统一(
/documents/{document_id}、/document-versions/{doc_ver_id}……),以/openapi.json为准。 - 时间戳 ISO8601 无时区后缀(如
2026-11-02T01:20:58.986077),按MAIN__TIMEZONE配置解释。 - 错误体分三层:网关 401 是纯文本
Unauthorized;业务层 401/403/404 是{"detail":"..."}字符串;FastAPI 校验 422 是{"detail":[{type,loc,msg,input,ctx}]}数组;个别 400/500 会直接泄漏 SQLAlchemy 异常全文(5.4 实测)。 - 写操作响应码与副作用可能不一致(标签自动创建实测 500 但生效),批量脚本务必回读验证。
- 过滤/排序参数:本文实测的列表端点未见 query 过滤参数(openapi 中 nodes/documents 列表仅分页类参数);检索过滤靠
q语法与用户隔离,标题/标签过滤实测只能拿到全量后自行筛选。
七、常见问题(API 坑实录)
问题 1:路径加 /v1 一律 404
/api/v1/tasks/ocr 实测返回 {"detail":"Not Found"}(webapp 日志 "POST /v1/tasks/ocr HTTP/1.0" 404)。3.5.3 的 OpenAPI 路径就没有版本段,nginx 剥掉 /api 后转发;一律按 /api/<openapi.json 里的路径> 写。
问题 2:登录用 form 编码 422
POST /api/token 只接受 JSON(-H 'Content-Type: application/json');x-www-form-urlencoded 实测 422。
问题 3:官方 create_token.sh 在 3.5.3 镜像内损坏
官方 REST API token 文档给的是 docker exec <webapp> create_token.sh <username>;实测报 could not find task "auth-cli"(脚本调 poetry task,镜像 pyproject 未注册),list_users.sh 同病。唯一可用路线:POST /api/token 换 JWT(4.1)。
问题 4:POST /api/nodes/ 忘带 parent_id
422 Field required(loc ["body","NewDocument","parent_id"])。父节点取 GET /api/users/me 的 inbox_folder_id(4.4)。
问题 5:DELETE /api/nodes/ 包对象报 422
{"node_ids":[...]} 实测 422 Input should be a valid list;裸 UUID 数组才 200(5.1)。
问题 6:给节点打标签传的是名字不是 UUID
PATCH /api/nodes/{id}/tags 的 array[string] 按标签名解释:传 UUID 会建出"名字是 UUID"的垃圾标签;传已存在的名字撞唯一约束 400;传新名字自动建默认色标签(实测 500 但生效)。正确姿势见 5.4。
问题 7:document-types 创建缺 custom_field_ids
name 与 custom_field_ids 均必填(空数组可);2.x 风格的 label/path 字段 3.5.3 不认(5.5 实测 422)。
问题 8:users 创建缺 role_ids/group_ids
均必填(空数组可),实测 422(5.7)。
问题 9:新建用户什么都调不通(403)
裸建用户 scopes:[],连 /users/me 都 403;token 长度骤减(257 vs 超管 729)可作快速判据。授权走 roles/groups(5.7)。
问题 10:OCR 返回 null ≠ 成功
POST /api/tasks/ocr 受理即 null;lang 在白名单但镜像缺 tessdata(如 jpn)时任务运行时 FAILURE。可用语言 = 24 码白名单 ∩ 镜像 5 语言包 = deu/eng/ron/spa(5.3 与《Papermerge 3.5.3 配置参数详解》5.4)。
问题 11:检索恒 0 命中
先怀疑索引层(Solr schema 缺 copyField / page_number 段冲突 / 误跑 webapp 的 index-schema apply),API 本身没错;修复与诊断链见部署指南第九章问题 4/6/7。
八、参考链接
- 实例自带规范:
GET http://<服务器>:8138/openapi.json(title "Papermerge DMS REST API",version 3.5.3,53 paths,匿名可读);swagger:/api/docs(需 token) - 官方 REST API 文档:https://docs.papermerge.io/latest/rest-api/overview/ 、rest-api/token/ 、rest-api/reference/ (查询日期 2026-11-01;token 页的 create_token.sh 已实测损坏,见问题 3)
- 官方架构文档(auth_request 网关设计):https://docs.papermerge.io/latest/developer-manual/architecture/
- GitHub 仓库:https://github.com/papermerge/papermerge-core
- 站内参考:《Papermerge 介绍与选型指南》《Papermerge 3.5.3 Docker Compose 部署指南》《Papermerge 3.5.3 配置参数详解》