SolidInvoice 3.1.2 API 使用详解
SolidInvoice 3.1.2 REST/GraphQL API 实战手册:X-API-TOKEN 鉴权与 Token 生命周期、内容协商、分页与过滤、客户/发票/税率 CRUD、状态流转、RFC 7807 错误处理与限流,全部接口实测于 2026-10-24。
SolidInvoice 3.1.2 API 使用详解
目标:面向需要与 SolidInvoice 3.1.2 做系统集成的开发者,讲清 API 鉴权、内容协商、分页过滤、资源 CRUD 与状态流转的完整用法。文中全部请求/响应均在 Ubuntu 22.04.4 LTS + SolidInvoice 3.1.2 实例上以 curl 实测通过,实测日期 2026-10-24;官方文档口径与实测不一致处均如实标注。实例部署见《SolidInvoice 3.1.2 Docker Compose 部署指南》,配置体系见《SolidInvoice 3.1.2 配置参数详解》,软件定位见《SolidInvoice 3.1.2 介绍与选型指南》。
一、环境信息
- 实例入口:
http://127.0.0.1:8136(生产替换为你的对外域名);运行时响应头X-Powered-By: PHP/8.5.12(实测) - API 规范版本:OpenAPI 3.2.0,规范标题
SolidInvoice,共 39 个路径(/api/docs.jsonopenapi实测) - 测试账号:
admin@example.com(演示口令q1w2e3r4,演示环境) - 测试 Token:
fb5ca8aaa2881db97b0dd4970a70262650771bfed3e4577780c192931df29400(64 位十六进制,名称 "Docs Integration Token",仅本演示实例有效;创建方式见第三章) - 测试数据:客户 3 家、发票 1 张(draft 状态,金额 100)
- 实测日期:2026-10-24
二、API 总览与资源清单
SolidInvoice 基于 API Platform 4 自动暴露三类机器接口:REST(/api/*)、GraphQL(/api/graphql)与面向 AI Agent 的 MCP 服务(/_mcp,见介绍篇架构章节)。本文以 REST 为主线,GraphQL 见第七章。
三个规范/调试入口全部实测可用:
| 入口 | 实测结果 | 说明 |
|---|---|---|
GET /api/docs | HTTP 200(470899 字节 HTML) | Swagger UI 交互页 |
GET /api/docs.jsonopenapi | HTTP 200(471442 字节) | OpenAPI 3.2.0 规范(/api/docs.json 等价) |
GET /api/docs.jsonld | HTTP 200(94197 字节) | Hydra 格式 API 文档(@context/@id/@type/title/entrypoint/supportedClass) |
OpenAPI 规范中的 39 个资源路径(实测枚举,按业务域分组):
| 业务域 | 路径 |
|---|---|
| 客户 | /api/clients、/api/clients/{id}、/api/clients/{clientId}/contacts、/api/clients/{clientId}/contact/{id}、/api/clients/{clientId}/addresses、/api/clients/{clientId}/address/{id}、/api/clients/{clientId}/invoices、/api/clients/{clientId}/quotes、/api/clients/{clientId}/payments、/api/clients/{clientId}/recurring-invoices |
| 发票 | /api/invoices、/api/invoices/{id}、/api/invoices/{id}/transitions/{transition}、/api/invoices/{invoiceId}/lines、/api/invoices/{invoiceId}/line/{id}、/api/invoices/{invoiceId}/payments |
| 报价 | /api/quotes、/api/quotes/{id}、/api/quotes/{id}/transitions/{transition}、/api/quotes/{id}/invoice(报价转发票)、/api/quotes/{quoteId}/lines、/api/quotes/{quoteId}/line/{id} |
| 周期发票 | /api/recurring-invoices、/api/recurring-invoices/{id}、/api/recurring-invoices/{id}/generate、/api/recurring-invoices/{id}/transitions/{transition}、/api/recurring-invoices/{invoiceId}/lines、/api/recurring-invoices/{invoiceId}/line/{id} |
| 支付与税率 | /api/payments、/api/payments/{id}、/api/taxes、/api/taxes/{id}、/api/tax-identifiers、/api/tax-identifiers/{id} |
| Token 与自定义字段 | /api/profile/api-tokens、/api/profile/api-tokens/{id}、/api/custom-fields、/api/custom-fields/reorder、/api/custom-fields/{id} |
⚠️ 注意:官方文档的资源列表写作
/api/api-tokens,而 3.1.2 OpenAPI 规范与实测可用路径是/api/profile/api-tokens,集成时以本文实测口径为准。
三、认证:API Token
3.1 Token 的形态与存储
- Token 为 64 位十六进制字符串(应用源码
ApiTokenManager:32 字节random_bytes转 hex)。 - 数据库只存 HMAC 哈希(源码
ApiTokenHasher:hash_hmac('sha256', 明文, 应用密钥))——明文仅在创建时显示一次,丢失后只能撤销重建。 - Token 与用户、公司绑定(
api_tokens表含user_id/company_id),因此 Token 的数据可见范围 = 所属公司的数据。 - 无独立 CLI 建 Token 命令(
solidinvoice console命令清单实测核过),创建走界面或下节的 HTTP 协议。
3.2 界面创建(常规路径)
登录后访问 Settings → API Keys(直达路径 /profile/api),点右上角绿色 + Create Token,填 Name(必填)与 Description,确认后明文 Token 只显示这一次。
3.3 无浏览器环境创建(Live Component 协议实测)
服务器自动化场景可以纯 HTTP 完成(Live Component 组件协议,实测全流程 2026-10-24):
- 登录:
GET /login取表单字段_csrf_token,随后POST /_login_check(form 字段_csrf_token/_username/_password),302 跳转/select-company,会话 Cookie 名SOLIDINVOICE_APP。 - 取组件属性:
GET /profile/api,页面中data-live-name-value="CreateApiToken"的节点携带data-live-props-value(URL 编码 JSON,含api_token.name/description/_token与校验字段@checksum)。 - 提交创建:
POST /_components/CreateApiToken/save(动作是请求路径后缀/save),请求头带Accept: application/vnd.live-component+html、X-Requested-With: XMLHttpRequest、X-Live-Url: /profile/api,multipart 字段data传 JSON:
{"props": <第 2 步原样 props>, "updated": {"api_token": {"name": "Docs Integration Token", "description": "Token for API usage documentation", "_token": "<props 中的 _token>"}}, "args": {}}
- 判定:成功 HTTP 200,响应 HTML 中
createdToken":"<64hex>即明文;若updated里漏带_token则 HTTP 422 且createdToken为 null(实测两个分支均复现)。
3.4 Token 管理接口
GET /api/profile/api-tokens 实测返回当前用户全部 Token(不含明文):
{"totalItems": 2, "member": [{"name": "probe-token", ...}, {"name": "Docs Integration Token", ...}]}
配套能力(官方 REST 文档口径):每次带 Token 的成功请求会被记录到该 Token 的请求历史(时间、方法、端点、状态码、IP、User-Agent,列表上限 100 条);鉴权失败请求不入历史;在 /profile/api 勾选 Token 执行 Revoke 后立即失效。
3.5 请求鉴权
Token 以 X-API-TOKEN 请求头传递。实测无 Token 请求:
curl -s "http://127.0.0.1:8136/api/invoices" -H "Accept: application/json"
实测回显:
{"title":"An error occurred","detail":"Full authentication is required to access this resource.","status":401,"type":"/errors/401"}
HTTP 401;REST 文档明确 API 为无状态鉴权,不涉及 CSRF(界面 Live Component 除外,见 3.3)。
四、内容协商与响应形态
同一资源按 Accept 头返回不同形态,全部实测:
| Accept | 实测结果 | 响应特征 |
|---|---|---|
application/ld+json(默认,不传 Accept 同此) | HTTP 200 | JSON-LD:@context/@id/@type + 集合 totalItems/member,日期为 ISO8601 带时区 |
application/json | HTTP 200 | 纯数组/对象(无 @context/@id 外壳),数字带小数位(100.0) |
application/vnd.api+json | HTTP 200 | JSON:API 形态:links/meta/data,meta 含 totalItems/itemsPerPage/currentPage |
application/hal+json | HTTP 400 | {"detail":"Serialization for the format \"hal\" is not supported.",...} |
⚠️ 注意:OpenAPI 规范的
Accept-Post响应头与官方文档都宣称支持 HAL,但 3.1.2 实测Accept: application/hal+json返回 400,集成时请用前三种形态。规范与实测不一致处以实测为准。
集合响应的 JSON-LD 形态实测(GET /api/invoices,节选):
{"@context":"/api/contexts/Invoice","@id":"/api/invoices","@type":"Collection","totalItems":1,"member":[{"@id":"/api/invoices/01M5PCZDXKWQ52GEG47T2ZG7BY","@type":"Invoice","status":"draft","id":"01M5PCZDXKWQ52GEG47T2ZG7BY","invoiceId":"1","client":"/api/clients/01M5PCZDX0FR1A7SXMY66AT427","balance":100,"lines":[...],"total":100}]}
关联资源一律以 IRI 字符串引用(client 指向 /api/clients/{ULID}),ULID 作为资源主键(01M5... 形态)。集合响应同时附带 search 字段声明可用的过滤参数(见第五章)。写操作允许的请求体格式由响应头 Accept-Post 声明(实测:application/ld+json, application/json, application/hal+json, application/vnd.api+json, application/xml, text/xml, text/html, multipart/form-data);Allow 头实测 OPTIONS, HEAD, GET, POST。
五、集合查询、分页与过滤
5.1 分页
- 集合默认页大小 30 条(JSON:API 形态
meta.itemsPerPage: 30实测;官方 REST 文档同为 30/页口径)。 - 翻页参数
?page=N实测有效:GET /api/clients?page=2返回currentPage: 2、空数据(实测库中仅 3 条客户,第 2 页为空,行为符合预期)。 ?itemsPerPage=N实测不生效:GET /api/clients?itemsPerPage=1仍返回全部 3 条且meta.itemsPerPage恒为 30;OpenAPI 规范的集合参数里也只有page没有itemsPerPage。官方文档提及该参数,3.1.2 实测仅page可用——按实测写。
5.2 过滤与排序
/api/invoices 集合响应内嵌的 search IriTemplate 实测声明了全部可用过滤/排序参数:
| 参数 | 类型 | 用途 |
|---|---|---|
status / status[] | 单值/多值 | 按发票状态过滤(draft/pending/paid/overdue/cancelled/archived) |
client / client[] | 单值/多值 | 按客户 IRI 过滤 |
invoiceDate[before] / invoiceDate[strictly_before] / invoiceDate[after] / invoiceDate[strictly_after] | 日期 | 开票日窗口(含/不含边界) |
due[before] / due[strictly_before] / due[after] / due[strictly_after] | 日期 | 到期日窗口 |
paidDate[before] / paidDate[strictly_before] / paidDate[after] / paidDate[strictly_after] | 日期 | 支付日窗口 |
order[invoiceDate] / order[due] / order[status] | 排序键 | 结果排序 |
示例:GET /api/invoices?status[]=draft&order[invoiceDate]=desc。
六、资源 CRUD 实战
以下用客户与发票两个最高频资源走通增删改查全流程;税率管理的补充实测见 6.6。
6.1 客户:查询与创建
查询集合(GET /api/clients,实测节选):
{"totalItems":3,"member":[
{"@id":"/api/clients/01M5PCZDX0FR1A7SXMY66AT427","name":"Test Client","contacts":[...],"invoices":[...],"quotes":[...]},
{"@id":"/api/clients/01M5PDG872KQQCC33XKTJPZ87M","name":"Acme Corporation"},
{"@id":"/api/clients/01M5PF9BPJ6PFCKQBHRG6KACQC","name":"API 测试客户"}]}
创建客户(POST /api/clients):
curl -s -X POST "http://127.0.0.1:8136/api/clients" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/ld+json" \
-d '{"name":"API 测试客户","currencyCode":"USD","website":"https://example.com"}'
实测 HTTP 201,返回完整资源(含 @id、status: active、credit: 0 等)。
6.2 发票:创建(含必踩的两个坑)
发票创建是最容易出错的接口,两个坑都实测复现:
坑一:users 必填且语义是"客户联系人"。 只带 client 与明细行会收到 422:
{"@type":"ConstraintViolation","status":422,
"violations":[{"propertyPath":"users","message":"You need to select at least 1 user to attach to the Invoice","code":"bef8e338-6ae5-4caf-b8e2-50e7b0579e69"}],
"detail":"users: You need to select at least 1 user to attach to the Invoice","type":"/validation_errors/bef8e338-6ae5-4caf-b8e2-50e7b0579e69","title":"An error occurred"}
OpenAPI 规范中 users 的示例值是 /api/clients/{uuid}/contact/{uuid} 形态的联系人 IRI 数组(发票的"收件人"),不是系统用户。联系人可从 /api/clients/{clientId}/contacts 查询(实测返回 email: client@example.com 的联系人 01M5PCZDX14RQ095CMS77P3RDX)。
坑二:关联资源只接受真实存在的 IRI。 传不存在的路径直接 400(实测):
{"@type":"Error","title":"An error occurred","detail":"Invalid IRI \"/api/notexist/123\".","status":400,"type":"/errors/400","description":"Invalid IRI \"/api/notexist/123\"."}
曾见集成方用 /api/users/<ULID> 充当收件人,同样得到 400 Invalid IRI——/api/users 根本不是 API 资源(39 个路径清单里没有),正确做法是 6.2 的联系人 IRI。
正确创建姿势(实测 HTTP 201):
curl -s -X POST "http://127.0.0.1:8136/api/invoices" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/ld+json" \
-d '{"client":"/api/clients/01M5PCZDX0FR1A7SXMY66AT427",
"users":["/api/clients/01M5PCZDX0FR1A7SXMY66AT427/contact/01M5PCZDX14RQ095CMS77P3RDX"],
"invoiceDate":"2026-10-24","due":"2026-11-07",
"lines":[{"name":"API 实测工时","description":"API 文档实测创建的明细行","price":250,"qty":2}],
"notes":"由 API 使用详解实测创建"}'
实测回显(关键字段):
{"status":"draft","total":500,"payableAmount":500,"@id":"/api/invoices/01M5PFKQ72P4RQD25TGCBWYDYT",
"lines":[{"name":"API 实测工时","price":250,"qty":2,"total":500}],
"balance":500,"due":"2026-11-07T00:00:00+00:00","notes":"由 API 使用详解实测创建"}
金额单位(实测一锤定音):官方文档称金额用最小货币单位(美分),这描述的是存储层;API JSON 层实测为主单位十进制——传 "price":1.23 创建发票后直查应用库 invoice_lines.price_amount 存储为 123(分),API 读取时又回显为 1.23。结论:JSON 收发用主单位(1.23 = 1.23 元),数据库与官方口径为分,集成时按 JSON 层实测口径处理,勿自行 ×100。
发票单条查询 GET /api/invoices/{id} 与客户同构,实测 200 返回完整资源(status/client/balance/lines/total/due 等)。
6.3 发票:更新(PATCH)
更新用 PATCH + Content-Type: application/merge-patch+json(RFC 7386 合并补丁,只发要改的字段):
curl -s -X PATCH "http://127.0.0.1:8136/api/invoices/$ID" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/merge-patch+json" \
-d '{"notes":"PATCH 实测更新","terms":"net_15"}'
实测 HTTP 200,回显 notes: PATCH 实测更新、terms: net_15,其余字段原样保留。
6.4 发票:状态流转(transitions)
发票状态机由 Symfony Workflow 驱动,API 入口为 POST /api/invoices/{id}/transitions/{transition}。实测三个流转:
curl -s -X POST "http://127.0.0.1:8136/api/invoices/$ID/transitions/accept" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/ld+json" -d '{}'
# => HTTP 201,回显 "status":"pending"(draft → pending)
curl -s -X POST "http://127.0.0.1:8136/api/invoices/$ID/transitions/pay" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/ld+json" -d '{}'
# => HTTP 201,回显 "status":"paid"(pending → paid)
非法流转返回 422 并说明原因(实测两例):
{"detail":"Transition \"pending\" cannot be applied to invoice in status \"draft\".","status":422,"type":"/errors/422"}
{"detail":"Transition \"cancel\" cannot be applied to invoice in status \"paid\".","status":422,"type":"/errors/422"}
完整状态机(源码 config/packages/pkg-workflow.php 与 src/InvoiceBundle/Model/Graph.php,3.1.2 tag 实证;流转名为 URL 中的实际字符串):
| 流转名 | 从(from) | 到(to) |
|---|---|---|
new | new | draft |
accept | new, draft | pending |
cancel | draft, pending, overdue | cancelled |
overdue | pending | overdue |
pay | pending, overdue | paid |
reopen | cancelled | draft |
archive | new, draft, cancelled, paid | archived |
edit | cancelled, draft, pending, overdue | draft |
即状态全集为 new → draft → pending → paid,旁路 overdue(逾期,由后台任务触发)、cancelled(可 reopen 回 draft 或 archive 归档)。报价单状态机同构(/api/quotes/{id}/transitions/{transition})。
6.5 发票:删除
DELETE /api/invoices/{id} 实测 HTTP 204;随后 GET 同一 id 实测 HTTP 404(detail: "Not Found"),删除即时生效。
6.6 税率:全套 CRUD
| 操作 | 实测请求 | 实测结果 |
|---|---|---|
| 列表 | GET /api/taxes | 200,totalItems 与 member(既有 VAT 20%,rate 20,type percentage) |
| 创建 | POST /api/taxes {"name":"VAT-测试","rate":13.0,"type":"standard"} | 201,返回含 ULID 的完整资源 |
| 更新 | PATCH /api/taxes/{id}(merge-patch+json {"name":"VAT-测试-改","rate":9.5}) | 200 |
| 删除 | DELETE /api/taxes/{id} | 204 |
OpenAPI 规范声明的字段枚举:type: Inclusive / Exclusive / Flat Rate;category: Standard / ZeroRated / Exempt / OutOfScope / ReverseCharge(category 默认 Standard)。实测服务端对 type 未做严格枚举校验(传小写 standard 亦 201 且原样存储),集成时建议按规范枚举取值以免界面显示异常。
七、GraphQL 端点
POST /api/graphql 实测可用(同样走 X-API-TOKEN 鉴权),返回 Relay 风格 edges/node 结构:
curl -s -X POST "http://127.0.0.1:8136/api/graphql" \
-H "X-API-TOKEN: $T" -H "Content-Type: application/json" \
-d '{"query":"{ clients { edges { node { name } } } }"}'
实测回显:
{"data":{"clients":{"edges":[{"node":{"name":"Test Client"}},{"node":{"name":"Acme Corporation"}},{"node":{"name":"API 测试客户"}}]}}}
查询不存在的字段时按 GraphQL over HTTP 惯例返回 HTTP 200 + errors 数组(实测):
{"errors":[{"message":"Cannot query field \"notAField\" on type \"Query\".","locations":[{"line":1,"column":3}]}]}
完整 Schema 可用控制台命令 api:graphql:export 导出(命令清单实测存在,见配置篇第八章),或在 Swagger 体系外用 GraphiQL 客户端自省。
八、错误处理与限流
8.1 错误体(RFC 7807 problem+json 实测)
四类错误的实测响应体:
// 401(无/错 Token)
{"title":"An error occurred","detail":"Full authentication is required to access this resource.","status":401,"type":"/errors/401"}
// 404(资源不存在)
{"@context":"/api/contexts/Error","@id":"/api/errors/404","@type":"Error","title":"An error occurred","detail":"Not Found","status":404,"type":"/errors/404","description":"Not Found"}
// 400(关联资源 IRI 非法)
{"@type":"Error","title":"An error occurred","detail":"Invalid IRI \"/api/notexist/123\".","status":400,"type":"/errors/400","description":"Invalid IRI \"/api/notexist/123\"."}
// 422(业务校验失败,ConstraintViolation 形态)
{"@type":"ConstraintViolation","status":422,"violations":[{"propertyPath":"users","message":"You need to select at least 1 user to attach to the Invoice"}],"detail":"users: You need to select at least 1 user to attach to the Invoice","type":"/validation_errors/bef8e338-6ae5-4caf-b8e2-50e7b0579e69"}
要点:application/ld+json 请求的错误体带 @context/@id/@type 全形,application/json 请求的错误体为精简形(401 实测即无外壳字段);422 的 violations[] 数组逐字段给出 propertyPath/message/code,type 指向可回查的 /api/validation_errors/{uuid}。
8.2 限流
官方 REST 文档口径:每 Token 300 请求/分钟滑动窗口,超限返回 429 + Retry-After 头,正常响应带 X-RateLimit-Limit/Remaining/Reset 头。3.1.2 实测:GET /api/invoices 完整响应头中没有任何 X-RateLimit-* 字段(实测头清单:Accept-Post/Allow/Cache-Control/Content-Type/Etag/Link/Vary/X-Content-Type-Options/X-Frame-Options/X-Powered-By);应用源码 pkg-rate_limiter.php 中存在 300 与 60 两档限流配置。结论:限流机制在代码中就位,但默认响应不带限流头——集成方按官方 300/分钟口径自设客户端节流即可,勿依赖响应头做自适应。
九、常见问题
Q1:所有请求都 401?
按序排查:请求头名必须是 X-API-TOKEN(不是 Authorization/Bearer);Token 是否已被撤销(撤销立即生效);Token 绑定的公司是否持有你要访问的数据(数据按公司隔离)。用 GET /api/profile/api-tokens 确认 Token 仍在列表。
Q2:400 Invalid IRI 怎么解?
关联字段只接受真实存在的资源 IRI。先查 39 路径清单(第二章)确认资源路径存在,再用集合接口拿到资源的 @id 原样回填。高频错误是把发票收件人填成 /api/users/<ULID>——正确字段是 users 数组 + 客户联系人 IRI(6.2)。
Q3:Accept: application/hal+json 报 400?
3.1.2 实测 HAL 序列化未启用(第四章实测表)。改用 application/ld+json、application/json 或 application/vnd.api+json。
Q4:金额到底传元还是分?
API JSON 层传主单位十进制(实测 price: 1.23 落库 123 分),6.2 有完整实验证据。官方文档"最小货币单位"的口径描述的是存储层,勿在 JSON 里 ×100。
Q5:itemsPerPage 参数不生效?
3.1.2 实测翻页仅 ?page=N 有效,页大小固定 30 条(第五章实测)。大批量导出用循环翻页,或改用 GraphQL 单请求聚合。
Q6:Token 明文忘了、只显示一次怎么办?
数据库只存 HMAC 哈希(3.1),明文不可恢复。在 /profile/api 撤销旧 Token 重建即可;重建不影响历史请求记录的归属。
Q7:创建发票返回 422 "You need to select at least 1 user"?
users 必填且要传客户联系人 IRI 数组(6.2 坑一)。先 GET /api/clients/{clientId}/contacts 拿联系人 @id,原样填入 users 数组再提交。
Q8:状态流转 422 "Transition cannot be applied"?
对照 6.4 的状态机表确认当前状态的合法去向(例如 paid 之后只能 archive);流转名用小写字符串(accept/pay/cancel),不要用状态名(pending 不是流转名,实测即报 422)。