开发者

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 方式用于服务端集成。

客户端脚本 / 服务端POST /auth/loginemail + passwordPonyLab API验证凭证生成 JWT Tokenaccess_token + refresh_tokenAuthorization: Bearer {access_token}受保护端点GET /experiments 等Access Token过期后用 refresh_token换取新 access_token

方式一:Bearer Token(推荐)

设置 → API Token 中生成长期 API Token,适合服务端集成和自动化脚本。

cURL

bash
curl https://api.ponylab.io/v1/experiments \
  -H "Authorization: Bearer pl_live_xxxx"

JavaScript / Node.js

javascript
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

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 调用其他端点。

bash
# 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..."

提示

Access Token 有效期为 1 小时,过期后用 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 与请求格式

text
https://api.ponylab.io/v1

私有化部署(企业版)使用安装时配置的域名,路径前缀相同(/v1)。

请求规范

请求头必填说明
AuthorizationBearer {token}Bearer Token 或 JWT
Content-TypePOST/PATCH 必填application/json文件上传使用 multipart/form-data
Acceptapplication/json默认即 JSON,通常不需要显式设置
X-Team-Id多团队时可选team_xxxxxxxxxx指定操作的目标团队。不传时使用 Token 关联的默认团队

响应格式

成功响应:

json
{
  "data": { ... },         // 单个资源
  // 或
  "data": [ ... ],         // 资源列表
  "total": 142,            // 总数(仅列表接口)
  "page": 1,
  "limit": 20
}

错误响应:

json
{
  "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 功能
*完整权限(等同于创建者角色)
bash
# 创建仅有实验只读权限的 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 / GLPRESEARCHER+
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/importCSV 批量导入样品(返回 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/importCSV 批量导入库存条目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-textAI 解析文本 → 结构化步骤(专业版)RESEARCHER+
POST/protocols/parse-pdfAI 解析 PDF → 结构化步骤(专业版)RESEARCHER+
Compliance 合规
GET/audit查询审计日志(支持 date/user/entity/action 过滤)PI+
GET/audit/export导出审计日志为 CSV 或 PDFPI+
GET/audit/verify验证 HMAC 链完整性SUPER_ADMIN
GET/capa列出 CAPA 记录PI+
POST/capa创建 CAPAPI+
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注册新 WebhookSUPER_ADMIN
GET/webhooks/:id获取 Webhook 详情(含投递统计)SUPER_ADMIN
PATCH/webhooks/:id更新 Webhook(URL/事件列表/启用状态)SUPER_ADMIN
DELETE/webhooks/:id删除 WebhookSUPER_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 — 用户登录

bash
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)

json
{
  "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 — 创建实验

json
// 请求 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)

json
{
  "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 — 电子签名

json
// 请求 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 — 记录库存调整

json
// 请求 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 — 实验数据对话

json
// 请求 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 — 自然语言查询

json
// 请求 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 — 创建升级会话

bash
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= 查询参数控制。

参数类型默认值最大值说明
pageinteger1页码,从 1 开始
limitinteger20100每页返回的资源数量

分页响应 Envelope:

json
{
  "data": [...],     // 当前页的数据数组
  "total": 142,      // 符合条件的总记录数
  "page": 2,         // 当前页码
  "limit": 20,       // 每页数量
  "hasMore": true    // 是否还有下一页(page * limit < total)
}

遍历所有数据的示例(Python):

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

过滤与排序

列表端点支持通过查询参数进行过滤和排序:

bash
# 过滤示例:获取已完成的实验,按更新时间降序
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)
免费版605,00010
专业版30050,00050
企业版1,000无限200

超出限流时,API 返回 HTTP 429 Too Many Requests,响应头包含重试信息:

text
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711234567     # Unix 时间戳,限流窗口重置时间
Retry-After: 12                    # 建议等待秒数

所有正常响应也包含当前用量头:

text
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1711234567

提示

批量操作(如全量导出)建议使用异步 job 接口(/export),而不是循环调用列表接口,以避免触发限流。

错误码参考

HTTP 状态码错误码含义处理建议
400VALIDATION_ERROR请求参数验证失败(缺少必填字段、格式错误等)检查 errors 数组中的具体字段错误
400INVALID_IMPORT_FORMATCSV 导入格式不符合模板要求下载最新导入模板后重新整理数据
401UNAUTHORIZED未提供认证凭证或凭证无效检查 Authorization 头,重新生成或刷新 Token
401TOKEN_EXPIREDJWT Access Token 已过期使用 Refresh Token 换取新的 Access Token
401TOKEN_REVOKEDToken 已被撤销在设置中重新生成 Token
403FORBIDDEN已认证但无权执行该操作(角色权限不足)确认用户角色是否满足端点最低权限要求
403PLAN_RESTRICTION该功能需要更高级的订阅计划升级到专业版或企业版
403TEAM_SUSPENDED团队账号被暂停(如欠费)前往设置恢复订阅或联系支持
404NOT_FOUND资源不存在,或不属于当前团队确认资源 ID 正确且属于当前 Token 关联的团队
409CONFLICT操作冲突(如预约时间段已被占用)查看 message 了解冲突详情,修改请求参数
422UNPROCESSABLE业务逻辑错误(如对已签名实验执行编辑)查看 message 了解违反的业务规则
422EXPERIMENT_ALREADY_SIGNED实验已签名,不允许修改内容如需修改,需要先走「修订」流程
422INSUFFICIENT_STOCK库存调整后数量将低于 0调整 quantity 或先补货
429RATE_LIMITED超出频率限制等待 Retry-After 秒数后重试
500INTERNAL_ERROR服务端内部错误联系支持,附上响应中的 requestId 值
503SERVICE_UNAVAILABLE服务临时不可用(维护或过载)查看状态页 status.ponylab.io,稍后重试

Webhook 集成指南

Webhook 允许 PonyLab 在事件发生时主动推送通知到你的服务器,无需轮询 API。

注册 Webhook

  1. 确保你的服务器端点可从公网访问,能够接收 HTTP POST 请求。
  2. 准备一个随机生成的 Webhook Secret(至少 32 位随机字符串),用于签名验证。
  3. 调用 API 注册 Webhook,或在 PonyLab 界面「系统 → 自动化 → Webhook」中配置。
bash
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_changedCAPA 状态发生变化
capa.overdueCAPA 截止日期已过
task.created新任务被创建
task.assigned任务被分配给成员
task.completed任务状态变为已完成
task.overdue任务截止日期已过
team.member_invited成员被邀请加入团队
team.member_joined成员接受邀请加入团队
team.member_removed成员被移除出团队

Webhook 负载格式

json
{
  "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 日志」中查看,包括每次投递的请求 headers/body 和响应详情,方便调试。

Webhook 签名验证

每个 Webhook POST 请求包含 X-PonyLab-Signature 请求头,值为用 Secret 对 raw request body 计算的 HMAC-SHA256。接收端必须在处理事件前验证签名,防止伪造请求。

验证代码示例

Node.js / Express

javascript
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

python
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

重要

验证签名时必须使用 raw request body(原始字节),不能先 JSON.parse 再序列化。Node.js 中使用 express.raw() 而不是 express.json()。Python Flask 中使用 request.get_data() 而不是 request.json

SDK 与客户端库

官方 SDK(即将推出)

PonyLab 官方 SDK 正在开发中,预计 2026 Q2 发布。届时将提供:

JavaScript / TypeScript

开发中

Python

开发中

R

计划中

在 SDK 发布之前

可以使用以下方式快速上手:

bash
# 使用 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

提示

OpenAPI Schema 可通过 api.ponylab.io/v1/openapi.json 下载,包含所有端点的完整 Schema 定义、请求/响应示例和 Zod/TypeScript 类型。

常见问题

免费版可以使用 API 吗?
是的,但只有只读权限(GET 端点),且每分钟限流 60 次。写操作(POST/PATCH/DELETE)需要专业版或企业版。AI 功能端点只对专业版开放。
如何知道我的 Token 关联的是哪个团队?
调用 GET /auth/me,响应中包含 currentTeamId 字段,即该 Token 当前关联的团队 ID。如果用户属于多个团队,可以通过 X-Team-Id 请求头指定目标团队。
API 支持 GraphQL 吗?
目前不支持,仅提供 REST API。GraphQL 支持在路线图上,但暂无确定的发布时间表。
如何处理异步操作(如 CSV 批量导入)?
批量操作返回 job_id。用 GET /samples/import/:job_id 轮询状态(建议每 2 秒查询一次)。状态为 COMPLETED 时,响应包含成功/失败行数和错误详情。也可以通过 Webhook 订阅 import.completed 事件,无需轮询。
API 有沙盒(测试)环境吗?
有。使用 https://api-sandbox.ponylab.io/v1,沙盒环境与生产环境数据完全隔离。沙盒 Token 以 pl_test_ 开头。注册时选择「沙盒账号」,或在设置中创建沙盒 Token。
如何批量操作(如一次性更新 100 个样品)?
大多数端点不支持批量写入,需要逐条调用。批量导入(CSV)是目前唯一的批量写入方式。如果需要批量更新,建议在非高峰时段运行,并在请求之间加入短暂延迟(如 50-100ms),避免触发限流。
Webhook 签名使用什么算法?
HMAC-SHA256。签名字符串格式为 sha256={hex_digest},其中 hex_digest 是用你的 Webhook Secret 对原始请求 body 字节计算的 HMAC-SHA256 十六进制摘要。
API Key 可以设置 IP 限制吗?
企业版支持在生成 Token 时绑定 IP 白名单,只允许来自指定 IP 地址的请求使用该 Token。在 POST /tokens 时传入 allowed_ips 字段(IP 地址数组)。

故障排查

问题:所有请求返回 401 Unauthorized

解决步骤

  1. 确认 Authorization 请求头格式:Authorization: Bearer pl_live_xxx(注意 Bearer 和 Token 之间有空格)
  2. 检查 Token 是否已在设置中被撤销(在 Token 列表中确认状态为 Active)
  3. 确认使用的是正确环境的 Token(生产 pl_live_ vs 沙盒 pl_test_)
  4. 如果使用 JWT,确认 Token 未过期(检查 expires_in 字段,调用 /auth/refresh 刷新)

问题:请求返回 403 Forbidden

解决步骤

  1. 确认 Token 关联的用户角色满足端点要求(参见端点表格「最低权限」列)
  2. 确认 Token 的 Scopes 包含所需权限(如 experiments:write)
  3. PLAN_RESTRICTION 错误意味着该功能需要更高订阅计划,检查团队订阅状态
  4. TEAM_SUSPENDED 意味着团队可能因为欠费被暂停,前往设置恢复订阅

问题:请求返回 404 Not Found(资源存在但 API 找不到)

解决步骤

  1. 确认资源 ID 正确(复制粘贴,避免手动输入出错)
  2. 确认资源属于当前 Token 关联的团队(团队间数据隔离,跨团队访问返回 404)
  3. 如果使用多团队账号,尝试在请求中附加 X-Team-Id 请求头指定正确团队

问题:Webhook 签名验证失败(始终返回 401)

解决步骤

  1. 确认使用 raw request body 计算签名,而不是解析后的 JSON
  2. 确认签名 Secret 与注册 Webhook 时使用的一致(没有多余空格)
  3. 确认从 X-PonyLab-Signature 头中正确提取了签名值(注意去掉 sha256= 前缀)
  4. 用「发送测试事件」功能(POST /webhooks/:id/test)在受控环境下调试

问题:分页请求返回的数据与预期不符(数量对不上)

解决步骤

  1. 注意 total 是符合过滤条件的总数,不是所有记录数
  2. 确认过滤参数的值格式正确(如日期必须是 ISO 8601 格式:2026-03-01T00:00:00Z)
  3. 避免在分页遍历过程中修改数据,否则可能导致同一条记录出现在多页或被跳过

问题:AI 端点返回 403 PLAN_RESTRICTION

解决步骤

  1. AI 功能仅对专业版和企业版开放,确认团队已升级
  2. 确认 SUPER_ADMIN 已在「设置 → AI 配置」中配置并验证了 API Key
  3. Token 的 Scopes 需要包含 ai:use

需要更多帮助?

完整的交互式 API 文档(含在线调试)请访问 api.ponylab.io/docs。集成问题请联系 [email protected],请在邮件中附上报错的 requestId 和请求示例,以便快速定位问题。