API 参考文档
通过程序化方式从你的 SignFastAi 模板发送文档。
SignFastAi 对外 API 可以让你从已保存的模板(仪表盘中的"我的模板")创建并发送文档、查询文档的签署状态、下载最终签署完成的 PDF——全程无需打开浏览器。它是为自动化场景设计的(Zapier 之类的脚本、你自己的工具),用于从其他系统触发签署请求。
概览
可用范围
对外 API 是 Pro / Lifetime 专属功能。每个接口都需要来自这两种套餐账号的有效 API 密钥。
基础地址
所有接口都在以下地址下:
https://<your-domain>/api/v1将 <your-domain> 替换为你访问 SignFastAi 时使用的域名。
快速示例
curl https://<your-domain>/api/v1/presets \
-H "x-api-key: YOUR_API_KEY"暂不支持的内容
这是一个刻意保持精简的接口集——创建/发送、查询状态、下载。目前还没有管理模板、客户或现场签署会话本身的接口;这些操作暂时只能在仪表盘中完成。
身份认证
创建密钥
- 登录后打开 设置 → API 密钥(
/settings/apikeys)。此页面需要 Pro 或 Lifetime 套餐——免费套餐账号会看到升级提示。 - 点击创建 API 密钥并为其命名。
- 复制对话框中显示的密钥值。之后你也可以随时在 API 密钥列表里再次显示和复制它(密钥列里的眼睛和复制图标),不必先另外保存。如果密钥泄露,请删除它并重新创建一个。
使用密钥
在每个请求中通过 x-api-key 请求头携带密钥:
curl https://<your-domain>/api/v1/presets \
-H "x-api-key: YOUR_API_KEY"不支持 Authorization: Bearer 方式——请使用 x-api-key。
速率限制
每个密钥限制为每 24 小时滚动窗口内 1000 次请求。这只是一个防滥用的兜底限制,并不是你真正的发送额度——你每月实际能发送的文档数量由你的套餐决定(见发送额度),与此处的请求速率上限相互独立。超出限制会返回 429——见错误代码。
认证错误
| 状态码 | 错误码 | 含义 |
|---|---|---|
| 401 | MISSING_API_KEY | 未发送 x-api-key 请求头 |
| 401 | INVALID_API_KEY(或其他密钥相关错误码) | 密钥不存在、已被撤销或格式不正确 |
| 403 | UPGRADE_REQUIRED | 密钥有效,但所属账号的套餐不包含 API 权限 |
| 429 | RATE_LIMITED | 当前 24 小时窗口内请求次数过多 |
模板
列出你的模板
GET /api/v1/presets返回你账号下保存的每一个模板(内部称为 "preset"),以及模板的签署角色。在调用 POST /api/v1/documents 之前你需要先查询这个接口——收件人是通过角色的 id 来匹配角色的,而不是通过角色的显示名称,所以你需要先在这里查到对应的 ID。
为什么用角色 ID 而不是角色名称来匹配? 同一个模板上的两个角色可能共用同一个显示名称(例如两个都叫"签署人"的角色)。ID 始终唯一,因此不会出现收件人被分配到哪个角色不明确的情况。
模板返回结果
{
"success": true,
"data": {
"presets": [
{
"id": "b3f6b6b0-...",
"title": "Freelance Agreement",
"roles": [
{
"id": "0e2b0f2e-...",
"role": "Client",
"requireAccessCode": false
},
{
"id": "9a1c9d3f-...",
"role": "Witness",
"requireAccessCode": true
}
]
}
]
}
}roles的顺序与模板中签署人的签署顺序一致。requireAccessCode: true表示分配到该角色的收件人在发送文档时必须同时提供accessCode——见访问码。
该接口不做分页——每个账号的模板数量都很少(免费套餐上限为 1 个;该接口本身也要求 Pro/Lifetime 套餐)。
文档
创建并发送
POST /api/v1/documents从模板创建一份文档并立即发送——作为本次调用的一部分,每位收件人都会收到签署链接邮件(除非模板开启了顺序签署,此时只会通知第一位签署人;其余签署人会在前一位完成后自动收到通知,与在仪表盘中发送的行为一致)。
请求体
{
"presetId": "b3f6b6b0-...",
"title": "Freelance Agreement — Acme Corp",
"recipients": [
{
"roleId": "0e2b0f2e-...",
"name": "Jane Client",
"email": "jane@acme.example"
},
{
"roleId": "9a1c9d3f-...",
"name": "Bob Witness",
"email": "bob@acme.example",
"accessCode": "4471"
}
]
}| 字段 | 是否必填 | 说明 |
|---|---|---|
presetId | 是 | 来自 GET /api/v1/presets |
title | 否 | 覆盖该文档使用的模板标题 |
recipients | 是 | 模板上每个角色对应一条记录——见下方说明 |
recipients[].roleId | 是 | 必须是属于 presetId 的角色 ID |
recipients[].name | 是 | |
recipients[].email | 是 | |
recipients[].accessCode | 视情况而定 | 如果该角色 requireAccessCode: true 则必填 |
模板上的每个角色都必须、且只能对应一位收件人。API 会在创建任何内容之前完成以下校验:
roleId不属于该presetId→UNKNOWN_ROLE_ID- 同一个
roleId被使用了两次 →DUPLICATE_ROLE_ID - 某个角色没有对应的收件人 →
MISSING_ROLE_RECIPIENT - 需要访问码的角色未提供(或提供了空值)
accessCode→ACCESS_CODE_REQUIRED
完整的错误返回格式见错误代码。
访问码
与仪表盘的批量发送功能不同(该功能只支持单角色模板,且会拒绝需要访问码的模板),API 支持多角色模板,并允许你直接提供每位收件人的访问码——由于没有 UI 环节来收集访问码,这部分信息必须由你自己提供。
发送额度
在创建文档之前,API 会检查你所在套餐的月度发送额度(与仪表盘发送时执行的检查相同)。如果发送这份文档会超出额度,会返回 409 SIGN_LIMIT_EXCEEDED,且不会创建任何内容。
创建返回结果
{
"success": true,
"data": {
"documentId": "c1d2e3f4-...",
"status": "sent",
"signers": [
{
"id": "...",
"name": "Jane Client",
"email": "jane@acme.example",
"role": "Client",
"status": "sent",
"signingUrl": "https://<your-domain>/sign/s/..."
},
{
"id": "...",
"name": "Bob Witness",
"email": "bob@acme.example",
"role": "Witness",
"status": "pending",
"signingUrl": null
}
]
}
}signingUrl 只会在本次调用中被通知到的签署人身上有值——如果某位签署人还在顺序签署的队列中排队等待,则为 null(轮到他们时会拿到真实链接并收到通知)。
查询状态
GET /api/v1/documents/:id返回文档当前的状态以及每位签署人的状态/时间戳。如果文档不存在或属于其他账号,都会返回 404(错误码 DOCUMENT_NOT_FOUND)——API 不会透露具体是哪种情况。
{
"success": true,
"data": {
"id": "c1d2e3f4-...",
"title": "Freelance Agreement — Acme Corp",
"status": "sent",
"sentAt": "2026-09-23T09:00:00.000Z",
"completedAt": null,
"signers": [
{
"id": "...",
"name": "Jane Client",
"email": "jane@acme.example",
"role": "Client",
"status": "viewed",
"viewedAt": "2026-09-23T09:05:00.000Z",
"signedAt": null,
"declinedAt": null,
"declineReason": null
}
]
}
}下载已签署的 PDF
GET /api/v1/documents/:id/download只有当 status 为 completed 时才可下载。直接返回最终的 PDF 文件(Content-Type: application/pdf),而不是 JSON 格式的返回体。在此之前调用会返回 409 DOCUMENT_NOT_COMPLETED。
签署后的 PDF 是在原文档基础上追加了一页签署证书。响应头说明:
Content-Disposition——原始文件名(文件名含非 ASCII 字符时,filename*里带有 UTF-8 编码的文件名)。X-Document-Sha256——文件的 SHA-256 值,可用来校验下载内容没有被篡改或截断。
从存储服务读取文件偶尔会临时失败(502 STORAGE_FETCH_FAILED,或 500)——直接重试即可,是安全的。
Webhook
无需再轮询 GET /documents/{id}:注册一个地址,你的任何文档(无论是通过
API、后台还是批量发送创建的)一有变化,我们就会向它 POST 一个事件。
在 设置 → API 密钥 → Webhook 中管理。最多可添加 5 个地址,每个地址可选择要接收的 事件,可以发送测试事件,并能查看 30 天内的每一次投递(包括对方返回的结果)。 Webhook 是 Pro / Lifetime 专属功能。
事件
| 事件 | 触发时机 |
|---|---|
document.sent | 文档已发出待签署 |
signer.viewed | 签署人打开了文档 |
signer.signed | 某位签署人完成签署 |
document.completed | 所有签署人都已签署,最终 PDF 已可下载 |
document.declined | 某位签署人拒绝签署 |
document.completed 只会在最终 PDF 生成之后才发送,所以收到后就可以立刻调用
GET /documents/{id}/download。
请求内容
每个请求是带 JSON 正文的 POST:
{
"id": "evt_2f6c1b0e-7a1d-4a55-9a52-2f7c0f8f1a11",
"type": "document.signed",
"createdAt": "2026-09-24T09:15:02.000Z",
"data": {
"document": {
"id": "0b0c7a4e-...",
"title": "服务协议 — Acme",
"status": "sent",
"createdVia": "api",
"sentAt": "2026-09-24T09:10:00.000Z",
"completedAt": null,
"finalFileHash": null,
"signers": [
{ "id": "…", "name": "Ada Lovelace", "email": "ada@example.com", "status": "signed" }
]
},
"signer": { "id": "…", "name": "Ada Lovelace", "email": "ada@example.com", "status": "signed" }
}
}signer.* 事件才会带 data.signer。同一个事件可能被多次投递(例如重试之后),
请把 id 当作幂等键。
校验签名
每个请求都带有以下请求头:
| 请求头 | 值 |
|---|---|
X-SignFast-Signature | t=<Unix 秒>,v1=<十六进制 HMAC-SHA256> |
X-SignFast-Event | 事件类型,如 document.completed |
X-SignFast-Delivery | 本次投递的唯一 ID |
签名是 HMAC-SHA256(secret, "<t>.<原始请求正文>"),其中 secret 是该地址的签名密钥
(创建时显示,之后随时可在地址列表中查看)。请对原始正文计算,用恒定时间比较,
并拒绝时间戳偏差超过几分钟的请求:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}投递与重试
请在 8 秒内返回任意 2xx 状态码表示已收到。其他状态码(或超时)都算失败:我们会在约
5 秒后和约 30 秒后各重试一次,之后将该次投递标记为失败。失败的投递可以在后台重新发送。
不会跟随重定向。
地址必须是 https:// 且公网可访问——内网地址、localhost 和云元数据网段会被拒绝。
调用记录
使用你的 API 密钥发出的每一次请求(方法、路径、状态码、错误码、耗时)都会被记录, 并显示在 设置 → API 密钥 → 调用记录 中,可按密钥和成功/出错筛选,保留 30 天。 API 密钥列表还会显示每个密钥的最近使用时间和近 30 天的调用次数;通过 API 创建的文档会在文档列表里带上 “API” 标记。
用量统计 标签会按小时(最近 24 小时)或按天(7 / 30 天)画出请求数图表,区分成功、客户端错误和服务端错误,并给出响应时间以及按接口、按密钥的分布。调用记录可以导出为 CSV;你还可以开启错误告警:最近一小时的失败率超过你设定的阈值时,我们会发邮件通知你(每 6 小时最多一封)。
错误代码
返回体格式
所有 JSON 返回(除了 PDF 下载接口)都使用相同的返回体格式:
{ "success": true, "data": { /* ... */ } }{
"success": false,
"error": {
"code": "MISSING_ROLE_RECIPIENT",
"message": "Every template role needs a recipient",
"details": { "missingRoleIds": ["9a1c9d3f-..."] }
}
}details 字段只会出现在那些有程序化可操作信息的错误里(比如具体是哪些角色 ID 出了问题、你的套餐额度是多少等)——建议先检查 details,再回退到解析 message。
错误码一览
| 状态码 | 错误码 | 出现位置 | 含义 |
|---|---|---|---|
| 401 | MISSING_API_KEY | 所有接口 | 未发送 x-api-key 请求头 |
| 401 | INVALID_API_KEY(或类似错误码) | 所有接口 | 密钥不存在、已被撤销或格式不正确 |
| 403 | UPGRADE_REQUIRED | 所有接口 | 密钥有效,但账号不是 Pro/Lifetime 套餐 |
| 429 | RATE_LIMITED | 所有接口 | 该密钥在过去 24 小时内请求次数超过 1000 次 |
| 400 | INVALID_BODY | POST /documents | 请求体未通过校验 |
| 404 | PRESET_NOT_FOUND | POST /documents | presetId 不存在或不属于你 |
| 400 | UNKNOWN_ROLE_ID | POST /documents | roleId 不属于该模板 |
| 400 | DUPLICATE_ROLE_ID | POST /documents | 同一个 roleId 被用于两位收件人 |
| 400 | MISSING_ROLE_RECIPIENT | POST /documents | 某个模板角色没有对应收件人 |
| 400 | ACCESS_CODE_REQUIRED | POST /documents | 需要访问码的角色缺少 accessCode |
| 409 | SIGN_LIMIT_EXCEEDED | POST /documents | 发送这份文档会超出套餐的月度发送额度 |
| 404 | DOCUMENT_NOT_FOUND | GET /documents/:id、.../download | 文档不存在或不属于你 |
| 409 | DOCUMENT_NOT_COMPLETED | GET /documents/:id/download | 尚未所有签署人都完成签署 |
| 502 | STORAGE_FETCH_FAILED | GET /documents/:id/download | 存储服务临时故障——请重试 |
| 500 | INTERNAL_ERROR | 所有接口 | 服务器发生未预期的错误 |