Plainpad 1.2.0 API 使用详解

Plainpad 1.2.0 REST API 全端点逐条实测:21 条路由总览、Bearer 认证与会话吊销闭环、notes/users/settings/密码找回/根资源五域请求回显、filter/sort/page/fields 查询助手、官方文档三处偏差与六条常见问题(2026-11-02)。

PlainpadPlainpadAPIRESTPHP自托管2026/11/02

Plainpad 1.2.0 API 使用详解

目标:把 Plainpad 1.2.0 的 REST API 全部端点逐个实测讲清——认证模型、五个资源域、查询助手与权限边界,并沉淀官方文档与实测不符的三处偏差。本文所有请求与回显均为 2026-11-02 在真实运行部署上的逐条实测。部署见 《Plainpad 1.2.0 Docker Compose 部署实战》 篇(本栏 B2),配置参数见 《Plainpad 1.2.0 配置参数详解》 篇(本栏 C1)。

一、环境信息

  • 被测部署:Docker Compose 项目 plainpad-1.2.0-stack,应用服务 plainpad(官方镜像 alextselegidis/plainpad:latest,应用版本 1.2.0,Laravel 12.69.0 / PHP 8.5.12)+ mysql:8.0(2026-11-02 php artisan about 实查)
  • API 基址:http://localhost:8139/api.php/v1(宿主 8139 → 应用服务 80)
  • 数据格式:JSON;认证:Authorization: Bearer {TOKEN} 请求头
  • 实验账号:管理员 admin@example.org / 12345678(官方播种,id 7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e);普通用户 root@example.org / q1w2e3r4(name "Test User",id 54ec677a-5344-4402-9189-bcd2e6470a28)
  • 取证方式:curl 逐条实测;端点清单以应用路由表(php artisan route:list)为权威依据

Plainpad 的服务端可编程面集中在 REST API:artisan 自定义命令只有 refresh 与 cleanup 两个(C1 篇 3.4 实测),笔记读写、用户管理、应用设置全部走 HTTP。

二、前置条件

  1. 部署按 B2 篇完成且服务健康:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8139/
# HTTP 200
  1. 主机备有 curl(本文命令在 Ubuntu 26.04.1 LTS 实测)。

  2. 至少一个可用账号。管理员账号用于 users/settings/根资源域;notes 域任意账号皆可(各账号数据严格隔离,普通用户只见自己的笔记)。

三、API 总览

应用路由表是端点的权威来源(官方文档存在滞后,见本章末):

cd /opt/data/quzuo/tmp/plainpad/stack
docker compose exec plainpad php artisan route:list --path=v1

实测回显(21 条):

  GET|HEAD  v1 ............................. V1\ApplicationController@retrieve
  POST      v1 .............................. V1\ApplicationController@install
  PUT       v1 ............................... V1\ApplicationController@update
  GET|HEAD  v1/notes ................................. V1\NotesController@list
  POST      v1/notes ............................... V1\NotesController@create
  GET|HEAD  v1/notes/{id} ........................ V1\NotesController@retrieve
  PUT       v1/notes/{id} .......................... V1\NotesController@update
  DELETE    v1/notes/{id} .......................... V1\NotesController@delete
  PATCH     v1/notes/{id}/pinned ................... V1\NotesController@pinned
  POST      v1/refresh ...................... V1\ApplicationController@refresh
  POST      v1/sessions ......................... V1\SessionsController@create
  DELETE    v1/sessions/{token} ................. V1\SessionsController@delete
  GET|HEAD  v1/settings ....................... V1\SettingsController@retrieve
  PUT       v1/settings ......................... V1\SettingsController@update
  GET|HEAD  v1/users ................................. V1\UsersController@list
  POST      v1/users ............................... V1\UsersController@create
  POST      v1/users/recovery ............. V1\UsersController@recoverPassword
  POST      v1/users/reset-password ......... V1\UsersController@resetPassword
  GET|HEAD  v1/users/{id} ........................ V1\UsersController@retrieve
  PUT       v1/users/{id} .......................... V1\UsersController@update
  DELETE    v1/users/{id} .......................... V1\UsersController@delete

按资源域归纳(认证列以实测为准):

域端点方法认证(实测)用途
应用/v1GET不需要(管理员才见 updates)应用信息与可用更新
应用/v1POST需要安装(建库表+播种)
应用/v1PUT需要执行自动更新
应用/v1/refreshPOST需要刷新安装(迁移+缓存)
会话/v1/sessionsPOST不需要登录换 token
会话/v1/sessions/{token}DELETE需要 Bearer 头吊销会话(登出)
笔记/v1/notesGET / POST需要列表 / 新建
笔记/v1/notes/{id}GET / PUT / DELETE需要读取 / 更新 / 删除
笔记/v1/notes/{id}/pinnedPATCH需要置顶开关
用户/v1/usersGET / POST需要(仅管理员)列表 / 新建
用户/v1/users/{id}GET / PUT / DELETE需要(仅管理员)读取 / 更新 / 删除
用户/v1/users/recoveryPOST不需要找回密码(发邮件)
用户/v1/users/reset-passwordPOST不需要用令牌重置密码
设置/v1/settingsGET / PUT需要(仅管理员)应用级设置读写

⚠️ 注意:官方 API 文档(仓库 docs/api.md,2026-11-02 实查)与实现存在三处偏差——① 密码恢复端点文档写 POST /v1/users/recover,实际是 POST /v1/users/recovery(按文档调用实测 405,见 5.5);② 文档称笔记更新用 POST /v1/notes/{ID},实际只接受 PUT(POST 实测 405,见 7.1);③ 文档称吊销会话「Authorization: Not required」,实测不带 Bearer 头返回 401(见 7.4)。PATCH /v1/notes/{id}/pinned、POST /v1/refresh、POST /v1/users/reset-password 三个已实现端点则未写进文档。以路由表与本文实测为准。

四、认证与首次调用

Plainpad 的 API 认证是「登录换 token + Bearer 头」模型:token 即会话凭证,有效期约 12 小时(创建响应中的 expires_at),除 sessions 创建与密码找回外所有端点都要带 Authorization: Bearer {TOKEN}。

4.1 创建会话(登录)

curl -s -X POST http://localhost:8139/api.php/v1/sessions \
  -H 'Content-Type: application/json' \
  -d '{"email":"root@example.org","password":"q1w2e3r4"}' \
  -w '\nHTTP %{http_code}\n'
{"user_id":"54ec677a-5344-4402-9189-bcd2e6470a28","expires_at":"2026-11-02 12:50:06","token":"EGRzpno23b9V3yKCEcj9Lmydlvdkdzt0Fn3fNJzmOnZvYoAmnwXMs4ZIS1IS"}
HTTP 201

响应三字段:user_id(账号 UUID)、expires_at(过期时刻)、token(后续请求凭据)。这正是网页登录框背后发出的请求(B2 篇 4.5 实测同源)。本文后续命令使用管理员 token 实值:

export TOKEN=FE5Is0r75P5jZoalOwHKpWi0NlAsAJwNGRWsRqqFNcwtwD9kWCLQk5RDEPxS

(该 token 由 admin@example.org / 12345678 登录获得,过期后重新执行 4.1 换取。)

4.2 错误凭据

curl -s -X POST http://localhost:8139/api.php/v1/sessions \
  -H 'Content-Type: application/json' \
  -d '{"email":"root@example.org","password":"wrongpass"}' \
  -w '\nHTTP %{http_code}\n'
{"message":"Invalid email or password."}
HTTP 401

4.3 首次带令牌调用

不带令牌访问受保护资源,实测统一 401:

curl -s http://localhost:8139/api.php/v1/notes -w '\nHTTP %{http_code}\n'
{
    "message": "Unauthenticated"
}
HTTP 401

带上令牌即 200(笔记域按账号隔离,管理员此时无笔记):

curl -s http://localhost:8139/api.php/v1/notes -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# []
# HTTP 200

4.4 吊销会话(登出)

curl -s -X DELETE http://localhost:8139/api.php/v1/sessions/EGRzpno23b9V3yKCEcj9Lmydlvdkdzt0Fn3fNJzmOnZvYoAmnwXMs4ZIS1IS \
  -H "Authorization: Bearer EGRzpno23b9V3yKCEcj9Lmydlvdkdzt0Fn3fNJzmOnZvYoAmnwXMs4ZIS1IS" \
  -w '\nHTTP %{http_code}\n'
HTTP 200

吊销后立即用同一 token 再请求,实测已失效(闭环验证):

curl -s http://localhost:8139/api.php/v1/notes -H "Authorization: Bearer EGRzpno23b9V3yKCEcj9Lmydlvdkdzt0Fn3fNJzmOnZvYoAmnwXMs4ZIS1IS" -w '\nHTTP %{http_code}\n'
{
    "message": "Unauthenticated"
}
HTTP 401

⚠️ 注意:DELETE 必须同时把待吊销 token 放在路径和 Bearer 头里(不带头的实测回显见第七章 7.4)。吊销后该 token 立即不可用,脚本化场景应在退出前主动调用。

五、各 API 域详解

5.1 Notes(笔记域,任意登录用户)

创建——payload 三字段 title/content/pinned,响应 201 返回完整笔记对象(实测三条,供后续演示):

curl -s -X POST http://localhost:8139/api.php/v1/notes \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"API 测试笔记一","content":"用于演示过滤与排序的第一条内容","pinned":false}' \
  -w '\nHTTP %{http_code}\n'
{"id":"38874e6b-bf4b-4246-a74d-a1d0b8085df5","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e00","content":"\u7528\u4e8e\u6f14\u793a\u8fc7\u6ee4\u4e0e\u6392\u5e8f\u7684\u7b2c\u4e00\u6761\u5185\u5bb9","pinned":false,"updated_at":"2026-11-02T00:50:22.000000Z","created_at":"2026-11-02T00:50:22.000000Z"}
HTTP 201

(另两条演示数据:{"title":"购物清单","content":"牛奶 鸡蛋 面包","pinned":true} → id 1582cda4-ba69-4370-ad8e-6f88e9f0a0c6;{"title":"API 测试笔记二","content":"第二篇测试内容"} → id 01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a,均 HTTP 201。中文在 JSON 响应中以 \uXXXX 转义回显,下同。)

列表:

curl -s http://localhost:8139/api.php/v1/notes -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
[{"id":"38874e6b-bf4b-4246-a74d-a1d0b8085df5","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","content":"\u7528\u4e8e\u6f14\u793a\u8fc7\u6ee4\u4e0e\u6392\u5e8f\u7684\u7b2c\u4e00\u6761\u5185\u5bb9","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e00","pinned":false,"created_at":"2026-11-02T00:50:22.000000Z","updated_at":"2026-11-02T00:50:22.000000Z"},{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","content":"\u7b2c\u4e8c\u7bc7\u6d4b\u8bd5\u5185\u5bb9","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c","pinned":false,"created_at":"2026-11-02T00:50:24.000000Z","updated_at":"2026-11-02T00:50:24.000000Z"},{"id":"1582cda4-ba69-4370-ad8e-6f88e9f0a0c6","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","content":"\u725b\u5976 \u9e21\u86cb \u9762\u5305","title":"\u8d2d\u7269\u6e05\u5355","pinned":true,"created_at":"2026-11-02T00:50:23.000000Z","updated_at":"2026-11-02T00:50:23.000000Z"}]
HTTP 200

⚠️ 注意:默认返回顺序既非时间也非置顶优先(上例 22s→24s→23s),脚本不要依赖默认序,显式加 sort 助手(第六章)。

读取单条:

curl -s http://localhost:8139/api.php/v1/notes/01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","created_at":"2026-11-02T00:50:24.000000Z","updated_at":"2026-11-02T00:50:24.000000Z","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c","content":"\u7b2c\u4e8c\u7bc7\u6d4b\u8bd5\u5185\u5bb9","pinned":false}
HTTP 200

更新——正确方法是 PUT(官方文档写 POST,实测 POST 返回 405,见 7.1):

curl -s -X PUT http://localhost:8139/api.php/v1/notes/01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"API 测试笔记二(已更新)","content":"更新后的内容","pinned":false}' \
  -w '\nHTTP %{http_code}\n'
{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","created_at":"2026-11-02T00:50:24.000000Z","updated_at":"2026-11-02T00:50:24.000000Z","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c\uff08\u5df2\u66f4\u65b0\uff09","content":"\u66f4\u65b0\u540e\u7684\u5185\u5bb9","pinned":false}
HTTP 200

置顶开关——独立端点,payload 必填 pinned(不带载荷实测 422):

curl -s -X PATCH http://localhost:8139/api.php/v1/notes/38874e6b-bf4b-4246-a74d-a1d0b8085df5/pinned -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# {"message":"The pinned field is required.","errors":{"pinned":["The pinned field is required."]}}
# HTTP 422

curl -s -X PATCH http://localhost:8139/api.php/v1/notes/38874e6b-bf4b-4246-a74d-a1d0b8085df5/pinned \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"pinned":true}' -w '\nHTTP %{http_code}\n'
# HTTP 200(成功响应体为空)

生效验证(配合 fields 助手,置顶笔记排到了列表首位):

curl -s "http://localhost:8139/api.php/v1/notes?fields=id,title,pinned" -H "Authorization: Bearer $TOKEN"
[{"id":"38874e6b-bf4b-4246-a74d-a1d0b8085df5","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e00","pinned":true},{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c\uff08\u5df2\u66f4\u65b0\uff09","pinned":false},{"id":"1582cda4-ba69-4370-ad8e-6f88e9f0a0c6","title":"\u8d2d\u7269\u6e05\u5355","pinned":true}]

删除:

curl -s -X DELETE http://localhost:8139/api.php/v1/notes/01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# HTTP 200(响应体为空)

错误路径——不存在与越权都是 404/401 组合:

curl -s http://localhost:8139/api.php/v1/notes/00000000-0000-0000-0000-000000000000 -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
{"message":"Not Found"}
HTTP 404

(三条演示笔记实验后已全部 DELETE 清理,终态 GET /v1/notes 回显 []。)

5.2 Users(用户域,仅管理员)

列表:

curl -s http://localhost:8139/api.php/v1/users -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
[{"id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","name":"Admin","email":"admin@example.org","admin":true,"locale":"en-US","view":"compact","line":"full","sort":"modified","theme":"light","encrypt":false,"created_at":"2026-11-02T00:20:21.000000Z","updated_at":"2026-11-02T00:48:23.000000Z"},{"id":"54ec677a-5344-4402-9189-bcd2e6470a28","name":"Test User","email":"root@example.org","admin":false,"locale":"en-US","view":"compact","line":"full","sort":"modified","theme":"light","encrypt":false,"created_at":"2026-11-02T00:21:48.000000Z","updated_at":"2026-11-02T00:21:48.000000Z"}]
HTTP 200

创建(新用户偏好全量给出,password 只进不出——任何响应都不回显口令):

curl -s -X POST http://localhost:8139/api.php/v1/users \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"API Temp","email":"api-temp@example.org","password":"q1w2e3r4","locale":"zh-CN","view":"compact","line":"full","sort":"modified","theme":"light","admin":false}' \
  -w '\nHTTP %{http_code}\n'
{"id":"20dd9c08-50c5-4c12-94ee-653396566774","name":"API Temp","email":"api-temp@example.org","locale":"zh-CN","view":"compact","line":"full","sort":"modified","theme":"light","encrypt":false,"admin":false,"updated_at":"2026-11-02T00:50:53.000000Z","created_at":"2026-11-02T00:50:53.000000Z"}
HTTP 201

更新(PUT,全量提交语义——漏字段即重置,详见 C1 篇 5.6):

curl -s -X PUT http://localhost:8139/api.php/v1/users/20dd9c08-50c5-4c12-94ee-653396566774 \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"API Temp Renamed","email":"api-temp@example.org","locale":"zh-CN","view":"compact","line":"full","sort":"modified","theme":"light","admin":false}' \
  -w '\nHTTP %{http_code}\n'
{"id":"20dd9c08-50c5-4c12-94ee-653396566774","created_at":"2026-11-02T00:50:53.000000Z","updated_at":"2026-11-02T00:50:53.000000Z","name":"API Temp Renamed","email":"api-temp@example.org","locale":"zh-CN","view":"compact","line":"full","sort":"modified","theme":"light","encrypt":false,"admin":false}
HTTP 200

权限边界——用普通用户(Test User)token 访问用户列表,实测不是 403 而是 401:

curl -s http://localhost:8139/api.php/v1/users -H "Authorization: Bearer $ROOT_TOKEN" -w '\nHTTP %{http_code}\n'
{"message":"Unauthorized"}
HTTP 401

删除(临时账号实验后删除,200 空体):

curl -s -X DELETE http://localhost:8139/api.php/v1/users/20dd9c08-50c5-4c12-94ee-653396566774 -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# HTTP 200

($ROOT_TOKEN 指 4.1 所得普通用户 token;401 语义与越权处理见 7.3。)

5.3 Settings(应用级设置,仅管理员)

读写九个键(default_locale + mail_* 八项,逐项含义见 C1 篇 5.5)。实测 GET 回显与 PUT 成功(PUT 成功响应体为空):

curl -s http://localhost:8139/api.php/v1/settings -H "Authorization: Bearer $TOKEN"
# {"mail_from_address":"notes@quzuo.net","mail_port":"1025","mail_password":"","mail_from_name":"Plainpad","default_locale":"zh-CN","mail_driver":"smtp","mail_host":"10.89.58.1","mail_encryption":"","mail_username":""}

curl -s -X PUT http://localhost:8139/api.php/v1/settings \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"default_locale":"zh-CN","mail_driver":"smtp","mail_host":"10.89.58.1","mail_port":"1025","mail_username":"","mail_password":"","mail_encryption":"","mail_from_address":"notes@quzuo.net","mail_from_name":"Plainpad"}' \
  -w '\nHTTP %{http_code}\n'
# HTTP 200

普通用户 PUT settings 实测同样 401 {"message":"Unauthorized"}(与 5.2 权限边界一致)。

5.4 Sessions 补充语义

token 背后是服务端会话记录(会话驱动 file,存应用 storage/ 内,C1 篇第一章 about 回显):重建应用即所有 token 失效,脚本需能处理「运行中突然 401」并重登。同一账号可并存多个 token(网页端与脚本各持一个互不影响),吊销只作用于指定 token。

5.5 密码找回两步(无需认证)

第一步 recovery(官方文档误写 recover,实测 405,见 7.2):

curl -s -X POST http://localhost:8139/api.php/v1/users/recovery \
  -H 'Content-Type: application/json' -d '{"email":"root@example.org"}' \
  -w '\nHTTP %{http_code}\n'
# HTTP 200(响应体为空)

200 只代表「已受理并尝试投递重置邮件」,不代表邮件送达。令牌以 bcrypt 哈希落 password_resets 表(实测查库):

docker compose exec mysql mysql -uroot -psecret -e "select email,token,created_at from plainpad.password_resets;"
email             token                                                                created_at
root@example.org  $2y$10$bxaLk8N41CtyLna5ljK83eVGdNqziT8nDqtLpySahI0MItsZURprm        2026-11-02 00:51:46

明文令牌只在邮件正文中,库里不可回查——没配好 SMTP(C1 篇 5.5)时找回流程实际走不通。

第二步 reset-password(携带邮件中的令牌;用无效令牌实测错误路径):

curl -s -X POST http://localhost:8139/api.php/v1/users/reset-password \
  -H 'Content-Type: application/json' \
  -d '{"email":"root@example.org","token":"badtoken","password":"q1w2e3r4"}' \
  -w '\nHTTP %{http_code}\n'
{"message":"Invalid or expired reset token."}
HTTP 422

(实验产生的 password_resets 行已清理。)

5.6 根资源(应用信息与安装/更新)

GET /v1——免认证可调,但 updates 只对管理员显示(源码逻辑:非管理员直接返回空对象)。用管理员 token 实测:

curl -s http://localhost:8139/api.php/v1 -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# []
# HTTP 200

空数组即「当前无可用更新」(更新源 APP_REPOSITORY 见 C1 篇 5.4)。

POST /v1(install)——对已安装实例是受保护的空操作,实测 401:

curl -s -X POST http://localhost:8139/api.php/v1 -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# {"message":"Unauthorized"}
# HTTP 401

源码给出双层守卫(控制器实查):表已存在即拒绝,防止 migrate:fresh 清空生产库——该端点只服务于「空库首装」场景,日常部署用不到(首装由入口脚本自动完成,B2 篇 4.4)。

PUT /v1(update)——触发自动更新,无新版本时实测 404:

curl -s -X PUT http://localhost:8139/api.php/v1 -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# {"message":"Not Found"}
# HTTP 404

404 语义 = 更新源没有比当前更新的版本。即使返回可更新,Docker 形态也不要调用它——更新写入应用文件系统,重建即丢(B2 篇第五节),正确升级路径是拉新镜像 + php artisan refresh。

POST /v1/refresh——升级后的刷新(迁移+缓存),实测 200 空体:

curl -s -X POST http://localhost:8139/api.php/v1/refresh -H "Authorization: Bearer $TOKEN" -w '\nHTTP %{http_code}\n'
# HTTP 200

六、分页/过滤/排序/裁剪通用约定

GET 类列表端点(notes/users)支持四个查询助手,均为实测。以 5.1 的三条演示笔记为数据:

助手参数行为实测备注
搜索filter=关键词按标题与内容模糊匹配中文需 URL 编码
排序sort=字段&direction=asc|desc按指定字段排序官方文档称最多三个字段依次生效
分页page=N&length=M第 N 页、每页 M 条length 默认 20
裁剪fields=a,b,c响应只留指定字段可与任意助手叠加

filter——?filter=购物(编码后)精确命中一条:

curl -s "http://localhost:8139/api.php/v1/notes?filter=%E8%B4%AD%E7%89%A9" -H "Authorization: Bearer $TOKEN"
[{"id":"1582cda4-ba69-4370-ad8e-6f88e9f0a0c6","user_id":"7a17c8fd-b84f-4f2c-8ba3-2a484c1a0e1e","content":"\u725b\u5976 \u9e21\u86cb \u9762\u5305","title":"\u8d2d\u7269\u6e05\u5355","pinned":true,"created_at":"2026-11-02T00:50:23.000000Z","updated_at":"2026-11-02T00:50:23.000000Z"}]

?filter=测试 则命中两条标题含「测试」的笔记(实测 200,输出节选见 5.1 数据结构)。

sort——按创建时间升序,顺序严格 22s→23s→24s:

curl -s "http://localhost:8139/api.php/v1/notes?sort=created_at&direction=asc&fields=id,title,created_at" -H "Authorization: Bearer $TOKEN"
[{"id":"38874e6b-bf4b-4246-a74d-a1d0b8085df5","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e00","created_at":"2026-11-02T00:50:22.000000Z"},{"id":"1582cda4-ba69-4370-ad8e-6f88e9f0a0c6","title":"\u8d2d\u7269\u6e05\u5355","created_at":"2026-11-02T00:50:23.000000Z"},{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a","title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c\uff08\u5df2\u66f4\u65b0\uff09","created_at":"2026-11-02T00:50:24.000000Z"}]

按标题降序(中文标题「购物清单」排到首):

curl -s "http://localhost:8139/api.php/v1/notes?sort=title&direction=desc&fields=title" -H "Authorization: Bearer $TOKEN"
# [{"title":"\u8d2d\u7269\u6e05\u5355"},{"title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e8c\uff08\u5df2\u66f4\u65b0\uff09"},{"title":"API \u6d4b\u8bd5\u7b14\u8bb0\u4e00"}]

page/length——三条数据按每页 2 条翻页:

curl -s "http://localhost:8139/api.php/v1/notes?page=1&length=2&fields=id" -H "Authorization: Bearer $TOKEN"
# [{"id":"38874e6b-bf4b-4246-a74d-a1d0b8085df5"},{"id":"01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a"}]

curl -s "http://localhost:8139/api.php/v1/notes?page=2&length=2&fields=id" -H "Authorization: Bearer $TOKEN"
# [{"id":"1582cda4-ba69-4370-ad8e-6f88e9f0a0c6"}]

fields——投影任意组合(上文已多次使用),批量同步场景只取 id,updated_at 可显著瘦身响应。

💡 技术拓展:REST 列表接口的分页模型常见三档——

模型代表特点
page/length 偏移分页Plainpad(本文)实现简单;深翻页性能差、并发写入下页界漂移
cursor 游标分页Memos/多数现代 API稳定不漂移,适合无限滚动
全量返回小型配置类 API无分页成本,数据量大时不可行

Plainpad 笔记量级(个人千条)下偏移分页足够;做增量同步时建议 sort=updated_at&direction=asc 配合 page 遍历,并容忍页界漂移。

七、常见问题

7.1 按官方文档 POST /v1/notes/{ID} 更新笔记返回 405

{
    "message": "The POST method is not supported for route v1/notes/01ee6648-c96c-4fe9-a4c6-aaaeea24ea8a. Supported methods: GET, HEAD, PUT, DELETE."
}
HTTP 405

文档滞后:更新笔记的正确方法是 PUT(5.1 实测 200)。405 响应里的 Supported methods 即权威提示。

7.2 按文档调 POST /v1/users/recover 返回 405

recover 会被路由匹配成 users/{id}(id=recover)的资源路由而 405。实际端点是 POST /v1/users/recovery(路由表实查,5.5 实测 200)。同理注意 reset-password 带连字符。

7.3 普通用户调管理端点返回 401 而不是 403

实测越权(非管理员访问 users 列表、settings 写入)统一返回 {"message":"Unauthorized"} + HTTP 401。写客户端时不要只按「401=未登录、403=无权限」的惯例分支——Plainpad 两种情况都是 401,需结合本地是否已持 token 判断是重登还是提示权限不足。

7.4 吊销会话不带 Authorization 头返回 401

curl -s -X DELETE http://localhost:8139/api.php/v1/sessions/EGRzpno23b9V3yKCEcj9Lmydlvdkdzt0Fn3fNJzmOnZvYoAmnwXMs4ZIS1IS -w '\nHTTP %{http_code}\n'
{
    "message": "Unauthenticated"
}
HTTP 401

文档称该端点无需认证,实测必须同时带 Bearer 头(4.4 实测 200 的写法)。

7.5 对已安装实例 POST /v1 返回 401

这是防误清库守卫(5.6 源码解释):表已存在即拒绝,即使管理员 token 也 401。首装请交给部署入口流程(B2 篇 4.4),不要脚本化调用该端点。

7.6 脚本运行中途突然全部 401

两种原因:token 过期(约 12 小时,4.1 响应 expires_at)或应用被重建(file 会话驱动,C1 篇第一章)。健壮脚本应在收到 401 时自动重登换新 token 重试一次,并避免把 token 写死在代码里。

八、参考链接

Plainpad 1.2.0 API 使用详解 - Quzuo