API 参考
PonyLab 提供完整的 RESTful HTTP API,所有端点返回 JSON。当前稳定版本为 v1。 本页涵盖认证、所有端点详情、代码示例、Webhook 集成、限流策略和错误码参考。
Base URL
api.ponylab.io/v1
协议
HTTPS Only
响应格式
application/json
时间格式
ISO 8601 UTC
认证
PonyLab API 支持两种认证方式,推荐 Bearer Token 方式用于服务端集成。
方式一:Bearer Token(推荐)
在 设置 → API Token 中生成长期 API Token,适合服务端集成和自动化脚本。
cURL
curl https://api.ponylab.io/v1/experiments \
-H "Authorization: Bearer pl_live_xxxx"JavaScript / Node.js
const res = await fetch(
"https://api.ponylab.io/v1/experiments",
{
headers: {
"Authorization": "Bearer pl_live_xxxx",
"Content-Type": "application/json",
},
}
);
const data = await res.json();Python
import requests
headers = {
"Authorization": "Bearer pl_live_xxxx",
"Content-Type": "application/json",
}
resp = requests.get(
"https://api.ponylab.io/v1/experiments",
headers=headers
)
data = resp.json()方式二:用户名/密码换取 JWT(动态认证)
适合需要代表用户动态调用 API 的场景。先调用 /auth/login 获取 Access Token,再用该 Token 调用其他端点。
# Step 1: 获取 Access Token
curl -X POST https://api.ponylab.io/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"your_password"}'
# Response:
# {
# "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# "refresh_token": "rt_xxxxxxxxxxxxxxxx",
# "expires_in": 3600
# }
# Step 2: 使用 Access Token
curl https://api.ponylab.io/v1/experiments \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."提示
refresh_token 换取新的 Access Token(调用 POST /auth/refresh),无需重新输入密码。方式三:Session Cookie(浏览器内使用)
用户已在浏览器中登录 PonyLab 时,Session Cookie 会自动随同源请求发送,无需额外配置。适合在浏览器扩展或内嵌 iframe 中使用 API。
安全注意事项
- 生产环境 Token 前缀为
pl_live_,测试沙盒为pl_test_ - 不要将 Token 提交到 Git 仓库、日志文件或硬编码在客户端代码中
- 使用环境变量(如
process.env.PONYLAB_API_TOKEN)管理 Token - 发现 Token 泄露时立即在设置中撤销并重新生成
- 为不同集成系统创建独立的 Token,泄露时可精准撤销单个 Token
Base URL 与请求格式
https://api.ponylab.io/v1私有化部署(企业版)使用安装时配置的域名,路径前缀相同(/v1)。
请求规范
| 请求头 | 必填 | 值 | 说明 |
|---|---|---|---|
| Authorization | 是 | Bearer {token} | Bearer Token 或 JWT |
| Content-Type | POST/PATCH 必填 | application/json | 文件上传使用 multipart/form-data |
| Accept | 否 | application/json | 默认即 JSON,通常不需要显式设置 |
| X-Team-Id | 多团队时可选 | team_xxxxxxxxxx | 指定操作的目标团队。不传时使用 Token 关联的默认团队 |
响应格式
成功响应:
{
"data": { ... }, // 单个资源
// 或
"data": [ ... ], // 资源列表
"total": 142, // 总数(仅列表接口)
"page": 1,
"limit": 20
}错误响应:
{
"error": "VALIDATION_ERROR",
"message": "title is required",
"errors": [
{ "field": "title", "message": "title is required" }
],
"requestId": "req_01HXYZ123"
}Token 权限范围(Scopes)
生成 API Token 时可以限定权限范围(Scopes),实现最小权限原则。未指定 Scopes 的 Token 具有与创建者角色相同的权限。
| Scope | 权限 |
|---|---|
| experiments:read | 查看实验(不含内容编辑) |
| experiments:write | 创建/编辑实验(包含 :read) |
| experiments:sign | 签署实验(需要 :write) |
| samples:read | 查看样品 |
| samples:write | 创建/编辑样品 |
| inventory:read | 查看库存 |
| inventory:write | 创建/编辑库存,记录调整 |
| tasks:read | 查看任务和项目 |
| tasks:write | 创建/编辑任务 |
| audit:read | 查看审计日志(仅 PI+ 角色可申请) |
| webhooks:manage | 管理 Webhook(仅 SUPER_ADMIN 可申请) |
| ai:use | 调用 AI 功能 |
| * | 完整权限(等同于创建者角色) |
# 创建仅有实验只读权限的 Token
curl -X POST https://api.ponylab.io/v1/tokens \
-H "Authorization: Bearer pl_live_xxxx" \
-H "Content-Type: application/json" \
-d '{"name":"CI Pipeline Token","scopes":["experiments:read","samples:read"]}'端点总览
下表列出所有端点。详细请求/响应 Schema 请查阅 交互式 OpenAPI 文档。
| 方法 | 路径 | 说明 | 最低权限 |
|---|---|---|---|
| Authentication 认证 | |||
| POST | /auth/register | 注册新账号(邮箱 + 密码) | 无 |
| POST | /auth/login | 邮箱/密码换取 Access Token + Refresh Token | 无 |
| POST | /auth/refresh | 刷新过期的 Access Token | 有效 Refresh Token |
| POST | /auth/logout | 使当前 Session 失效(撤销 Refresh Token) | 已认证 |
| GET | /auth/me | 获取当前登录用户的账号信息 | 已认证 |
| PATCH | /auth/me | 更新当前用户的姓名、头像等信息 | 已认证 |
| POST | /auth/change-password | 修改密码(需要提供旧密码验证) | 已认证 |
| Teams 团队 | |||
| GET | /teams | 获取当前用户所属的所有团队 | 已认证 |
| POST | /teams | 创建新团队 | 已认证 |
| GET | /teams/:id | 获取团队详情 | 成员 |
| PATCH | /teams/:id | 更新团队名称、描述、可见性等设置 | SUPER_ADMIN |
| DELETE | /teams/:id | 删除团队(不可逆,包含所有数据) | SUPER_ADMIN |
| GET | /teams/:id/members | 获取团队成员列表 | 成员 |
| POST | /teams/:id/invitations | 发送邮件邀请 | SUPER_ADMIN |
| PATCH | /teams/:id/members/:uid | 更改成员角色 | SUPER_ADMIN |
| DELETE | /teams/:id/members/:uid | 移除成员 | SUPER_ADMIN |
| GET | /teams/:id/ai-settings | 获取 AI 配置(不含原始 Key) | SUPER_ADMIN |
| PUT | /teams/:id/ai-settings | 更新 AI 提供商和模型配置 | SUPER_ADMIN |
| Experiments 实验 | |||
| GET | /experiments | 列出实验(分页,支持 status/direction_id/assignee 过滤) | TECHNICIAN+ |
| POST | /experiments | 创建新实验 | RESEARCHER+ |
| GET | /experiments/:id | 获取单个实验详情(含内容、签名、样品关联) | TECHNICIAN+ |
| PATCH | /experiments/:id | 更新实验元数据或内容(编辑器内容) | RESEARCHER+ |
| DELETE | /experiments/:id | 移入回收站 | SUPER_ADMIN |
| POST | /experiments/:id/sign | 为实验添加电子签名(需要密码二次验证) | RESEARCHER+ |
| POST | /experiments/:id/witness | 添加见证签名 | PI+ |
| PATCH | /experiments/:id/status | 更新实验状态(草稿→进行中→完成) | RESEARCHER+ |
| GET | /experiments/:id/versions | 获取版本历史列表 | TECHNICIAN+ |
| GET | /experiments/:id/versions/:vid | 获取特定版本的内容快照 | TECHNICIAN+ |
| POST | /experiments/:id/attachments | 上传附件(支持 CSV/PDF/图片) | RESEARCHER+ |
| DELETE | /experiments/:id/attachments/:aid | 删除附件 | RESEARCHER+ |
| POST | /experiments/:id/export | 导出实验为 PDF / CSV / JSON / GLP | RESEARCHER+ |
| Samples 样品 | |||
| GET | /samples | 列出样品(支持 type/status/storage/experiment_id 过滤) | TECHNICIAN+ |
| POST | /samples | 创建新样品 | TECHNICIAN+ |
| GET | /samples/:id | 获取单个样品详情 | TECHNICIAN+ |
| PATCH | /samples/:id | 更新样品字段 | TECHNICIAN+ |
| DELETE | /samples/:id | 删除样品 | PI+ |
| GET | /samples/:id/lineage | 获取完整谱系树(父/子样品关系) | TECHNICIAN+ |
| POST | /samples/:id/derive | 从现有样品派生新样品(子样品) | TECHNICIAN+ |
| POST | /samples/import | CSV 批量导入样品(返回 job_id,异步处理) | RESEARCHER+ |
| GET | /samples/import/:job_id | 查询批量导入任务进度和结果 | RESEARCHER+ |
| Inventory 库存 | |||
| GET | /inventory | 列出库存条目(支持 type/location/low_stock 过滤) | TECHNICIAN+ |
| POST | /inventory | 创建库存条目 | RESEARCHER+ |
| GET | /inventory/:id | 获取单个库存条目(含当前数量和调整历史) | TECHNICIAN+ |
| PATCH | /inventory/:id | 更新库存条目信息(名称、位置、阈值等) | RESEARCHER+ |
| DELETE | /inventory/:id | 删除库存条目 | PI+ |
| POST | /inventory/:id/adjust | 记录库存调整(type: IN/OUT/ADJUST/DISCARD) | TECHNICIAN+ |
| GET | /inventory/:id/adjustments | 获取调整历史(分页) | TECHNICIAN+ |
| GET | /inventory/:id/forecast | 获取 AI 库存预测(专业版) | RESEARCHER+ |
| POST | /inventory/import | CSV 批量导入库存条目 | RESEARCHER+ |
| Instruments 仪器 | |||
| GET | /instruments | 列出仪器 | TECHNICIAN+ |
| POST | /instruments | 添加仪器 | RESEARCHER+ |
| GET | /instruments/:id | 获取仪器详情 | TECHNICIAN+ |
| PATCH | /instruments/:id | 更新仪器信息 | RESEARCHER+ |
| GET | /instruments/:id/bookings | 获取仪器预约列表(支持日期范围过滤) | TECHNICIAN+ |
| POST | /instruments/:id/bookings | 创建预约 | TECHNICIAN+ |
| PATCH | /instruments/:id/bookings/:bid | 更新预约(时间/备注) | TECHNICIAN+ |
| DELETE | /instruments/:id/bookings/:bid | 取消预约 | TECHNICIAN+ |
| GET | /instruments/:id/qualifications | 获取 IQ/OQ/PQ 鉴定记录 | RESEARCHER+ |
| POST | /instruments/:id/qualifications | 创建鉴定记录 | PI+ |
| GET | /instruments/:id/maintenance | 获取维护记录列表 | TECHNICIAN+ |
| POST | /instruments/:id/maintenance | 记录维护事件 | RESEARCHER+ |
| Tasks 任务 | |||
| GET | /projects | 列出项目(研究方向) | TECHNICIAN+ |
| POST | /projects | 创建项目 | RESEARCHER+ |
| GET | /projects/:id | 获取项目详情(含任务列表) | TECHNICIAN+ |
| PATCH | /projects/:id | 更新项目(名称、描述、状态) | RESEARCHER+ |
| DELETE | /projects/:id | 删除项目 | PI+ |
| GET | /tasks | 列出任务(可按 project_id/assignee/status/due_date 过滤) | TECHNICIAN+ |
| POST | /tasks | 创建任务 | RESEARCHER+ |
| GET | /tasks/:id | 获取任务详情(含子任务和关联实验) | TECHNICIAN+ |
| PATCH | /tasks/:id | 更新任务(状态/负责人/截止日期/优先级) | RESEARCHER+ |
| DELETE | /tasks/:id | 删除任务 | PI+ |
| Protocols 协议 | |||
| GET | /protocols | 列出协议(支持 status/category 过滤) | TECHNICIAN+ |
| POST | /protocols | 创建协议草稿 | RESEARCHER+ |
| GET | /protocols/:id | 获取协议详情(含步骤列表) | TECHNICIAN+ |
| PATCH | /protocols/:id | 更新协议草稿内容 | RESEARCHER+ |
| POST | /protocols/:id/publish | 发布协议(锁定当前版本) | PI+ |
| POST | /protocols/:id/executions | 基于协议创建执行记录 | TECHNICIAN+ |
| GET | /protocols/:id/executions | 获取该协议的执行历史 | TECHNICIAN+ |
| POST | /protocols/parse-text | AI 解析文本 → 结构化步骤(专业版) | RESEARCHER+ |
| POST | /protocols/parse-pdf | AI 解析 PDF → 结构化步骤(专业版) | RESEARCHER+ |
| Compliance 合规 | |||
| GET | /audit | 查询审计日志(支持 date/user/entity/action 过滤) | PI+ |
| GET | /audit/export | 导出审计日志为 CSV 或 PDF | PI+ |
| GET | /audit/verify | 验证 HMAC 链完整性 | SUPER_ADMIN |
| GET | /capa | 列出 CAPA 记录 | PI+ |
| POST | /capa | 创建 CAPA | PI+ |
| GET | /capa/:id | 获取 CAPA 详情 | PI+ |
| PATCH | /capa/:id | 更新 CAPA(状态/根因/措施/关闭) | PI+ |
| POST | /reports/glp | 生成 GLP 合规报告(PDF) | PI+ |
| POST | /reports/gmp | 生成 GMP 合规报告(PDF) | PI+ |
| Billing 计费 | |||
| GET | /billing/subscription | 获取当前订阅状态和计划信息 | SUPER_ADMIN |
| POST | /billing/checkout | 创建 Stripe Checkout 会话(返回 checkout_url) | SUPER_ADMIN |
| POST | /billing/portal | 创建 Stripe 客户门户会话(返回 portal_url) | SUPER_ADMIN |
| GET | /billing/usage | 获取当月 API 用量和 AI token 用量统计 | SUPER_ADMIN |
| AI 功能 | |||
| POST | /ai/chat | 实验数据对话(需要 experiment_id + messages) | RESEARCHER+ |
| POST | /ai/report | 生成实验报告草稿 | RESEARCHER+ |
| POST | /ai/anomaly | 对 CSV 附件运行异常检测 | RESEARCHER+ |
| POST | /ai/query | 自然语言数据查询(NL2SQL) | RESEARCHER+ |
| POST | /ai/forecast/:inventory_id | 生成单个库存条目的预测 | RESEARCHER+ |
| Webhooks | |||
| GET | /webhooks | 列出已配置的 Webhook 端点 | SUPER_ADMIN |
| POST | /webhooks | 注册新 Webhook | SUPER_ADMIN |
| GET | /webhooks/:id | 获取 Webhook 详情(含投递统计) | SUPER_ADMIN |
| PATCH | /webhooks/:id | 更新 Webhook(URL/事件列表/启用状态) | SUPER_ADMIN |
| DELETE | /webhooks/:id | 删除 Webhook | SUPER_ADMIN |
| GET | /webhooks/:id/deliveries | 查看投递历史(含请求/响应详情) | SUPER_ADMIN |
| POST | /webhooks/:id/test | 发送测试事件验证端点 | SUPER_ADMIN |
| Calendar 日历 | |||
| GET | /calendar/events | 获取日历事件(仪器预约 + 任务截止日) | TECHNICIAN+ |
| GET | /calendar/bookings | 获取指定时间范围内的仪器预约列表 | TECHNICIAN+ |
| Export 导出 | |||
| POST | /export/experiments | 批量导出实验(返回 job_id,异步) | RESEARCHER+ |
| GET | /export/:job_id | 查询导出任务状态和下载链接 | RESEARCHER+ |
| POST | /export/audit | 导出审计日志 | PI+ |
| API Tokens | |||
| GET | /tokens | 列出当前用户的 API Token | 已认证 |
| POST | /tokens | 生成新 API Token(只在创建时返回原始值) | 已认证 |
| DELETE | /tokens/:id | 撤销 API Token | 已认证 / SUPER_ADMIN |
端点详细说明
认证端点示例
POST /auth/login — 用户登录
curl -X POST https://api.ponylab.io/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "your_secure_password"
}'响应(200 OK)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_01HXYZ123456789",
"expires_in": 3600,
"token_type": "Bearer",
"user": {
"id": "user_abc123",
"email": "[email protected]",
"name": "Dr. Zhang Wei",
"currentTeamId": "team_xyz789"
}
}实验端点示例
POST /experiments — 创建实验
// 请求 Body
{
"title": "HEK293T 细胞 MTT 活力检测 — 化合物 A",
"description": "评估化合物 A 对 HEK293T 的细胞毒性",
"status": "IN_PROGRESS",
"directionId": "dir_abc", // 研究方向 ID
"sampleIds": ["smp_001", "smp_002"],
"tags": ["MTT", "细胞毒性", "化合物A"]
}响应(201 Created)
{
"data": {
"id": "exp_01HXYZ789",
"title": "HEK293T 细胞 MTT 活力检测 — 化合物 A",
"status": "IN_PROGRESS",
"createdBy": { "id": "user_abc", "name": "Dr. Zhang Wei" },
"createdAt": "2026-03-20T08:30:00Z",
"updatedAt": "2026-03-20T08:30:00Z",
"directionId": "dir_abc",
"sampleIds": ["smp_001", "smp_002"],
"signatureCount": 0,
"attachmentCount": 0
}
}POST /experiments/:id/sign — 电子签名
// 请求 Body(需要密码二次验证)
{
"password": "your_current_password",
"role": "AUTHOR", // AUTHOR | WITNESS
"statement": "I confirm this experiment is accurate and complete."
}
// 响应(200 OK)
{
"data": {
"signatureId": "sig_01HXYZ",
"experimentId": "exp_01HXYZ789",
"signedBy": { "id": "user_abc", "name": "Dr. Zhang Wei" },
"role": "AUTHOR",
"signedAt": "2026-03-20T10:00:00Z",
"hash": "sha256:a3f8b2c1..."
}
}库存调整端点示例
POST /inventory/:id/adjust — 记录库存调整
// 请求 Body
{
"type": "OUT", // IN | OUT | ADJUST | DISCARD
"quantity": 5, // 调整数量(OUT/DISCARD 为扣除量,ADJUST 为绝对值)
"unit": "mL",
"reason": "Western Blot 实验使用",
"experimentId": "exp_01HXYZ789", // 可选:关联到具体实验
"date": "2026-03-20T09:00:00Z"
}
// 响应(200 OK)
{
"data": {
"adjustmentId": "adj_01",
"inventoryId": "inv_xxx",
"type": "OUT",
"quantity": 5,
"previousQuantity": 50,
"newQuantity": 45,
"performedBy": { "id": "user_abc", "name": "Dr. Zhang Wei" },
"createdAt": "2026-03-20T09:00:00Z"
}
}AI 功能端点示例
POST /ai/chat — 实验数据对话
// 请求 Body
{
"experimentId": "exp_01HXYZ789",
"messages": [
{
"role": "user",
"content": "请分析 CSV 附件中各组的均值和标准差,判断是否有统计学差异"
}
],
"model": "claude-3-5-sonnet-20241022" // 可选,覆盖团队默认模型
}
// 响应(200 OK)
{
"data": {
"message": {
"role": "assistant",
"content": "根据上传的 CSV 数据分析...[完整 AI 回复]"
},
"usage": {
"input_tokens": 3240,
"output_tokens": 512
}
}
}POST /ai/query — 自然语言查询
// 请求 Body
{
"query": "上个月所有使用 HEK293T 细胞的已完成实验",
"returnSql": true // 可选:在响应中返回生成的 SQL(供调试)
}
// 响应(200 OK)
{
"data": {
"results": [
{ "id": "exp_001", "title": "...", "completedAt": "2026-02-15T..." },
...
],
"total": 7,
"generatedSql": "SELECT * FROM experiments WHERE ...", // 仅 returnSql=true 时返回
"executionTime": "142ms"
}
}计费端点示例
POST /billing/checkout — 创建升级会话
curl -X POST https://api.ponylab.io/v1/billing/checkout \
-H "Authorization: Bearer pl_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"plan": "PRO",
"successUrl": "https://app.ponylab.io/settings/billing?success=true",
"cancelUrl": "https://app.ponylab.io/settings/billing"
}'
# 响应
# {
# "data": {
# "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_..."
# }
# }
# 将用户重定向到 checkoutUrl 完成支付分页
所有列表接口使用基于页码的分页,通过 ?page= 和 ?limit= 查询参数控制。
| 参数 | 类型 | 默认值 | 最大值 | 说明 |
|---|---|---|---|---|
| page | integer | 1 | — | 页码,从 1 开始 |
| limit | integer | 20 | 100 | 每页返回的资源数量 |
分页响应 Envelope:
{
"data": [...], // 当前页的数据数组
"total": 142, // 符合条件的总记录数
"page": 2, // 当前页码
"limit": 20, // 每页数量
"hasMore": true // 是否还有下一页(page * limit < total)
}遍历所有数据的示例(Python):
import requests
def get_all_experiments(token):
"""获取当前团队所有实验"""
results = []
page = 1
while True:
resp = requests.get(
f"https://api.ponylab.io/v1/experiments",
params={"page": page, "limit": 100, "status": "COMPLETED"},
headers={"Authorization": f"Bearer {token}"}
)
data = resp.json()
results.extend(data["data"])
if not data.get("hasMore"):
break
page += 1
return results过滤与排序
列表端点支持通过查询参数进行过滤和排序:
# 过滤示例:获取已完成的实验,按更新时间降序
GET /experiments?status=COMPLETED&sort=updatedAt&order=desc
# 支持的通用过滤参数:
# status= 资源状态(具体值参见各端点文档)
# createdAt_gte= 创建时间 >=(ISO 8601 格式)
# createdAt_lte= 创建时间 <=
# updatedAt_gte= 更新时间 >=
# q= 全文搜索(在标题和描述中搜索)
# sort= 排序字段(createdAt / updatedAt / title / status)
# order= 排序方向 asc | desc(默认 desc)提示
q= 参数进行全文搜索时,PonyLab 使用 Meilisearch 进行语义化搜索,支持拼音、近义词和错别字容错。限流策略
API 请求按 Token(未认证请求按 IP)进行频率限制:
| 订阅计划 | 每分钟请求数 | 每天请求数 | 突发上限(5s) |
|---|---|---|---|
| 免费版 | 60 | 5,000 | 10 |
| 专业版 | 300 | 50,000 | 50 |
| 企业版 | 1,000 | 无限 | 200 |
超出限流时,API 返回 HTTP 429 Too Many Requests,响应头包含重试信息:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711234567 # Unix 时间戳,限流窗口重置时间
Retry-After: 12 # 建议等待秒数所有正常响应也包含当前用量头:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1711234567提示
/export),而不是循环调用列表接口,以避免触发限流。错误码参考
| HTTP 状态码 | 错误码 | 含义 | 处理建议 |
|---|---|---|---|
| 400 | VALIDATION_ERROR | 请求参数验证失败(缺少必填字段、格式错误等) | 检查 errors 数组中的具体字段错误 |
| 400 | INVALID_IMPORT_FORMAT | CSV 导入格式不符合模板要求 | 下载最新导入模板后重新整理数据 |
| 401 | UNAUTHORIZED | 未提供认证凭证或凭证无效 | 检查 Authorization 头,重新生成或刷新 Token |
| 401 | TOKEN_EXPIRED | JWT Access Token 已过期 | 使用 Refresh Token 换取新的 Access Token |
| 401 | TOKEN_REVOKED | Token 已被撤销 | 在设置中重新生成 Token |
| 403 | FORBIDDEN | 已认证但无权执行该操作(角色权限不足) | 确认用户角色是否满足端点最低权限要求 |
| 403 | PLAN_RESTRICTION | 该功能需要更高级的订阅计划 | 升级到专业版或企业版 |
| 403 | TEAM_SUSPENDED | 团队账号被暂停(如欠费) | 前往设置恢复订阅或联系支持 |
| 404 | NOT_FOUND | 资源不存在,或不属于当前团队 | 确认资源 ID 正确且属于当前 Token 关联的团队 |
| 409 | CONFLICT | 操作冲突(如预约时间段已被占用) | 查看 message 了解冲突详情,修改请求参数 |
| 422 | UNPROCESSABLE | 业务逻辑错误(如对已签名实验执行编辑) | 查看 message 了解违反的业务规则 |
| 422 | EXPERIMENT_ALREADY_SIGNED | 实验已签名,不允许修改内容 | 如需修改,需要先走「修订」流程 |
| 422 | INSUFFICIENT_STOCK | 库存调整后数量将低于 0 | 调整 quantity 或先补货 |
| 429 | RATE_LIMITED | 超出频率限制 | 等待 Retry-After 秒数后重试 |
| 500 | INTERNAL_ERROR | 服务端内部错误 | 联系支持,附上响应中的 requestId 值 |
| 503 | SERVICE_UNAVAILABLE | 服务临时不可用(维护或过载) | 查看状态页 status.ponylab.io,稍后重试 |
Webhook 集成指南
Webhook 允许 PonyLab 在事件发生时主动推送通知到你的服务器,无需轮询 API。
注册 Webhook
- 确保你的服务器端点可从公网访问,能够接收 HTTP POST 请求。
- 准备一个随机生成的 Webhook Secret(至少 32 位随机字符串),用于签名验证。
- 调用 API 注册 Webhook,或在 PonyLab 界面「系统 → 自动化 → Webhook」中配置。
curl -X POST https://api.ponylab.io/v1/webhooks \
-H "Authorization: Bearer pl_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP 集成通知",
"url": "https://your-server.example.com/ponylab/events",
"events": [
"experiment.signed",
"sample.created",
"inventory.low_stock",
"capa.created",
"task.overdue"
],
"secret": "your_32_char_random_secret_here_xxxx",
"active": true
}'可订阅的事件列表
| 事件名称 | 触发时机 |
|---|---|
| experiment.created | 新实验被创建 |
| experiment.updated | 实验内容或元数据被更新 |
| experiment.status_changed | 实验状态发生变化 |
| experiment.signed | 实验被电子签名(作者或见证人) |
| experiment.deleted | 实验被移入回收站 |
| sample.created | 新样品被创建 |
| sample.updated | 样品信息被更新 |
| sample.deleted | 样品被删除 |
| inventory.created | 新库存条目被创建 |
| inventory.adjusted | 库存数量被调整(IN/OUT/ADJUST) |
| inventory.low_stock | 库存低于安全库存阈值 |
| inventory.expired | 库存条目到期(based on expiry_date) |
| instrument.booking_created | 仪器预约被创建 |
| instrument.booking_cancelled | 仪器预约被取消 |
| instrument.maintenance_due | 仪器维护到期提醒(提前 7 天) |
| capa.created | 新 CAPA 被创建 |
| capa.status_changed | CAPA 状态发生变化 |
| capa.overdue | CAPA 截止日期已过 |
| task.created | 新任务被创建 |
| task.assigned | 任务被分配给成员 |
| task.completed | 任务状态变为已完成 |
| task.overdue | 任务截止日期已过 |
| team.member_invited | 成员被邀请加入团队 |
| team.member_joined | 成员接受邀请加入团队 |
| team.member_removed | 成员被移除出团队 |
Webhook 负载格式
{
"id": "evt_01HXYZ123456",
"event": "experiment.signed",
"timestamp": "2026-03-20T10:30:00.000Z",
"teamId": "team_abc123",
"teamName": "王教授课题组",
"data": {
"experimentId": "exp_xyz789",
"experimentTitle": "PCR BRCA1 扩增",
"signatureId": "sig_001",
"signedBy": {
"id": "user_abc",
"name": "Dr. Zhang Wei",
"email": "[email protected]",
"role": "RESEARCHER"
},
"signatureRole": "AUTHOR",
"signedAt": "2026-03-20T10:29:58.000Z"
},
"apiVersion": "v1"
}重试策略
PonyLab 对失败的 Webhook 投递(非 2xx 响应码,或超过 10 秒无响应)自动重试,最多 5 次,使用指数退避:
| 重试次数 | 等待时间 | 距首次失败 |
|---|---|---|
| 第 1 次重试 | 1 秒 | 1 秒 |
| 第 2 次重试 | 5 秒 | 6 秒 |
| 第 3 次重试 | 30 秒 | 36 秒 |
| 第 4 次重试 | 2 分钟 | ~2.5 分钟 |
| 第 5 次重试 | 10 分钟 | ~12.5 分钟 |
| 放弃 | — | 5 次重试全部失败后标记为 dead |
提示
Webhook 签名验证
每个 Webhook POST 请求包含 X-PonyLab-Signature 请求头,值为用 Secret 对 raw request body 计算的 HMAC-SHA256。接收端必须在处理事件前验证签名,防止伪造请求。
验证代码示例
Node.js / Express
const crypto = require("crypto");
function verifyPonyLabWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// 使用 timingSafeEqual 防止时序攻击
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature.replace("sha256=", ""), "hex")
);
}
// Express 中间件示例
app.post(
"/ponylab/events",
express.raw({ type: "application/json" }), // 必须用 raw body
(req, res) => {
const sig = req.headers["x-ponylab-signature"];
if (!verifyPonyLabWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
console.log("Received:", event.event, event.id);
// 立即返回 200,异步处理事件
res.status(200).send("OK");
processEvent(event); // 异步处理
}
);Python / Flask
import hmac
import hashlib
from flask import Flask, request, abort
import os
app = Flask(__name__)
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
# 移除 "sha256=" 前缀(如果存在)
sig = signature.replace("sha256=", "")
return hmac.compare_digest(expected, sig)
@app.route("/ponylab/events", methods=["POST"])
def webhook():
signature = request.headers.get("X-PonyLab-Signature", "")
if not verify_signature(
request.get_data(),
signature,
os.environ["WEBHOOK_SECRET"]
):
abort(401)
event = request.json
print(f"Received: {event['event']} - {event['id']}")
# 处理事件...
return "OK", 200重要
express.raw() 而不是 express.json()。Python Flask 中使用 request.get_data() 而不是 request.json。SDK 与客户端库
官方 SDK(即将推出)
PonyLab 官方 SDK 正在开发中,预计 2026 Q2 发布。届时将提供:
JavaScript / TypeScript
开发中
Python
开发中
R
计划中
在 SDK 发布之前
可以使用以下方式快速上手:
# 使用 openapi-generator 从 OpenAPI Schema 自动生成客户端
npx openapi-generator-cli generate \
-i https://api.ponylab.io/v1/openapi.json \
-g typescript-fetch \
-o ./ponylab-client
# 或者使用 Python 版本
pip install openapi-python-client
openapi-python-client generate \
--url https://api.ponylab.io/v1/openapi.json提示
常见问题
免费版可以使用 API 吗?
如何知道我的 Token 关联的是哪个团队?
API 支持 GraphQL 吗?
如何处理异步操作(如 CSV 批量导入)?
API 有沙盒(测试)环境吗?
如何批量操作(如一次性更新 100 个样品)?
Webhook 签名使用什么算法?
API Key 可以设置 IP 限制吗?
故障排查
问题:所有请求返回 401 Unauthorized
解决步骤
- 确认 Authorization 请求头格式:Authorization: Bearer pl_live_xxx(注意 Bearer 和 Token 之间有空格)
- 检查 Token 是否已在设置中被撤销(在 Token 列表中确认状态为 Active)
- 确认使用的是正确环境的 Token(生产 pl_live_ vs 沙盒 pl_test_)
- 如果使用 JWT,确认 Token 未过期(检查 expires_in 字段,调用 /auth/refresh 刷新)
问题:请求返回 403 Forbidden
解决步骤
- 确认 Token 关联的用户角色满足端点要求(参见端点表格「最低权限」列)
- 确认 Token 的 Scopes 包含所需权限(如 experiments:write)
- PLAN_RESTRICTION 错误意味着该功能需要更高订阅计划,检查团队订阅状态
- TEAM_SUSPENDED 意味着团队可能因为欠费被暂停,前往设置恢复订阅
问题:请求返回 404 Not Found(资源存在但 API 找不到)
解决步骤
- 确认资源 ID 正确(复制粘贴,避免手动输入出错)
- 确认资源属于当前 Token 关联的团队(团队间数据隔离,跨团队访问返回 404)
- 如果使用多团队账号,尝试在请求中附加 X-Team-Id 请求头指定正确团队
问题:Webhook 签名验证失败(始终返回 401)
解决步骤
- 确认使用 raw request body 计算签名,而不是解析后的 JSON
- 确认签名 Secret 与注册 Webhook 时使用的一致(没有多余空格)
- 确认从 X-PonyLab-Signature 头中正确提取了签名值(注意去掉 sha256= 前缀)
- 用「发送测试事件」功能(POST /webhooks/:id/test)在受控环境下调试
问题:分页请求返回的数据与预期不符(数量对不上)
解决步骤
- 注意 total 是符合过滤条件的总数,不是所有记录数
- 确认过滤参数的值格式正确(如日期必须是 ISO 8601 格式:2026-03-01T00:00:00Z)
- 避免在分页遍历过程中修改数据,否则可能导致同一条记录出现在多页或被跳过
问题:AI 端点返回 403 PLAN_RESTRICTION
解决步骤
- AI 功能仅对专业版和企业版开放,确认团队已升级
- 确认 SUPER_ADMIN 已在「设置 → AI 配置」中配置并验证了 API Key
- Token 的 Scopes 需要包含 ai:use
需要更多帮助?
完整的交互式 API 文档(含在线调试)请访问 api.ponylab.io/docs。集成问题请联系 [email protected],请在邮件中附上报错的 requestId 和请求示例,以便快速定位问题。