用 DocuSign API 创建 composite template(组合模板)并注入服务端文档,核心只有一个请求:POST /restapi/v2.1/accounts/{accountId}/envelopes,在请求体中携带 compositeTemplates 数组。数组中的每一项由三个构件叠加而成:一个 serverTemplates 元素,引用你 DocuSign 账户中已保存的模板;一个 document(或 documents)对象,以 Base64 携带运行时生成或上传的文件;以及提供签署人、角色和签署控件的 inlineTemplates。由于组合模板可以把模板中的静态文档替换为你服务器刚生成的文件,它是将动态文档与预配置签署逻辑合并的标准模式。
本文将逐个拆解 JSON 结构,给出一条完整的 curl 请求,再用 Python 演示同一次调用,最后附上一张选型决策表和一份排错清单,覆盖最容易让团队踩坑的错误。
什么是 DocuSign 组合模板,什么时候需要它
普通的模板 envelope 请求只会在 envelope 定义的顶层传一个 templateId,再配合 templateRoles 填入签署人。当模板内的文档是固定不变的时候,这种方式很好用:你建模板时上传的那份 PDF,就是每位签署人收到的 PDF。
局限出现在应用需要在运行时生成文档的场景。用 CRM 数据预填的合同、按候选人生成的录用通知、按客户拼装的发票——这些文件在设计模板时都不存在,自然也无法放进模板里。组合模板解决的正是这个问题:它允许一个 envelope 定义同时组合:
- 服务端模板(server-side templates):你已在 DocuSign 账户中配置好的模板(文档、签署人路由、签署控件、合并域)。
- 服务端文档(server-side documents):在请求本身中提供的文档,可以是 Base64 编码的单个
document对象,也可以是documents数组。 - 内联模板(inlineTemplates):直接在 JSON 中定义或覆盖签署人与控件。
关键行为是文档替换。当请求中提供的文档与所引用服务端模板内某个文档使用相同的 documentId 时,DocuSign 会用你的运行时文件原位替换模板中的静态文件,同时保留模板的签署人和控件。如果 documentId 与模板内任何文档都不匹配,这份文档就只是被追加到 envelope 中。这套"替换或追加"机制正是关键词里"server side documents"的含义:字节流由你的服务器提供,签署逻辑由模板提供。
如果你的团队还在权衡是否值得投入这种深度的 API 集成,不妨先退一步,看看更宏观的集成电子签名 API 的收益,再决定采用哪种 envelope 模式。
compositeTemplates 的 JSON 结构剖析
根据 DocuSign eSignature REST API 参考(v2.1),compositeTemplates 是 envelope 定义上的顶层数组。每个元素的结构如下:
```json
{
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "YOUR_TEMPLATE_ID"
}
],
"document": {
"documentId": "1",
"name": "agreement.pdf",
"fileExtension": "pdf",
"documentBase64": "JVBERi0xLjQK..."
},
"inlineTemplates": [
{
"sequence": "2",
"recipients": {
"signers": [
{
"recipientId": "1",
"roleName": "Client",
"name": "Ada Lovelace",
"email": "ada@example.com"
}
]
}
}
]
}
]
}
```
逐字段说明:
compositeTemplateId:该组合模板的可选字符串标识,当你需要在请求的其他位置引用它时很有用。serverTemplates:已存放在你账户中的模板数组。每一项携带一个sequence(形如"1"的字符串)和一个templateId(模板详情页上显示的 GUID)。模板会贡献它的文档、签署人、控件和路由顺序。document:随请求提供的单个文档,包含documentId、name、fileExtension和documentBase64。如果想在同一个组合模板下挂载多份运行时文档,改用documents数组——一个组合模板不应同时携带两者。inlineTemplates:内联定义数组。每一项有自己的sequence,可以携带documents、recipients、customFields,以及挂在签署人上的控件定义。这里的签署人会按roleName(以及recipientId)与服务端模板的签署人匹配合并,这就是你不动模板、在请求时填入姓名和邮箱的方式。recipients与tabs:在inlineTemplates内部,签署人对象(signer、抄送人等)接受与其他任何地方相同的控件数组:signHereTabs、dateSignedTabs、textTabs、fullNameTabs等等。
有两条顺序规则必须注意。第一,同一个组合模板内 serverTemplates 与 inlineTemplates 的 sequence 值必须唯一,它们共同决定叠加顺序——对同一角色或字段,后出现的项会覆盖先出现的项。第二,文档在最终 envelope 中的排列顺序由 documentId 决定,而不是数组的书写顺序。刻意规划好 ID,能避免签署人翻阅文件包时的意外。
控件是两个世界交汇的地方。服务端模板中定义的控件会延续到被替换后的文档上。使用 anchorString 定位的控件(在文档中搜索文本,例如 "anchorString": "Please sign here:")会在你的运行时文件中重新附着到该文本出现的位置;而使用固定 xPos/yPos 坐标的控件会把同样的坐标套用到新文件上,这只有在你生成的文档与原版布局完全一致时才有效。当运行时文档的布局会变化时,优先使用锚点字符串。
完整示例:用服务端文档替换模板文档
下面是一条针对 DocuSign 开发者沙箱(demo.docusign.net)的完整可运行请求。它取一份内部含有 documentId 为 "1" 的文档的协议模板,用你服务器刚生成的 PDF 替换该文档,通过内联模板填入签署人信息,然后发送 envelope。
首先是 envelope 定义,保存为 envelope.json:
```json
{
"emailSubject": "Your agreement is ready to sign",
"status": "sent",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{
"sequence": "1",
"templateId": "YOUR_TEMPLATE_ID"
}
],
"document": {
"documentId": "1",
"name": "agreement-generated.pdf",
"fileExtension": "pdf",
"documentBase64": "JVBERi0xLjQK..."
},
"inlineTemplates": [
{
"sequence": "2",
"recipients": {
"signers": [
{
"recipientId": "1",
"roleName": "Client",
"name": "Ada Lovelace",
"email": "ada@example.com"
}
]
}
}
]
}
]
}
```
然后是 curl 调用:
```bash
curl --request POST \
"https://demo.docusign.net/restapi/v2.1/accounts/YOUR_ACCOUNT_ID/envelopes" \
--header "Authorization: Bearer
--header "Content-Type: application/json" \
--data @envelope.json
```
请求成功会返回 201 Created,其中包含新 envelope 的 envelopeId、URI、状态,以及一份可供记录审计的 recipients 摘要。
三条实现要点:
documentId匹配才会触发替换。 上面的"1"必须等于你想替换的模板内文档的documentId。把它改成一个未被占用的 ID(比如"2"),你的文件就会被追加到模板文档旁边——有时这正是你要的,更多时候则是个意外。- 用
status: "created"先建草稿 envelope,发送前可以检查。这是在引入真实签署人之前,以最低成本肉眼确认控件落点的方式。 - Base64 负载会很大。 一份几 MB 的 PDF 会让 JSON 请求体膨胀约三分之一;如果请求体积成为瓶颈,请查阅官方文档当前提供的传输方案,例如分块上传。
你可以在这个骨架上继续叠加。往 serverTemplates 里再加一项(比如 sequence 为 "3"),就能把两份已存模板合并进一个 envelope——当一个文件包需要同时包含保密协议模板和服务协议模板时很好用。在内联模板的签署人下添加控件,例如 "tabs": { "signHereTabs": [{ "anchorString": "Client signature:", "anchorXOffset": "0", "anchorYOffset": "0", "anchorUnits": "pixels" }] },就能让签名位置由运行时文档中的文本驱动,而不是固定坐标。另外,如果你的签署流程运行在自己的 Web 应用里、而不是走邮件,那么嵌入式发送与远程发送 API 的区别决定了你是生成 recipient view URL,还是让 DocuSign 直接给签署人发邮件。
用 Python 发起同样的请求
下面用 Python 的 requests 完成完全相同的调用:读取本地 PDF、编码、发送 envelope:
```python
import base64
import json
import requests
API_BASE = "https://demo.docusign.net/restapi/v2.1"
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"
TEMPLATE_ID = "YOUR_TEMPLATE_ID"
with open("agreement.pdf", "rb") as f:
pdf_base64 = base64.b64encode(f.read()).decode("ascii")
envelope = {
"emailSubject": "Your agreement is ready to sign",
"status": "sent",
"compositeTemplates": [
{
"compositeTemplateId": "1",
"serverTemplates": [
{"sequence": "1", "templateId": TEMPLATE_ID}
],
"document": {
"documentId": "1",
"name": "agreement-generated.pdf",
"fileExtension": "pdf",
"documentBase64": pdf_base64,
},
"inlineTemplates": [
{
"sequence": "2",
"recipients": {
"signers": [
{
"recipientId": "1",
"roleName": "Client",
"name": "Ada Lovelace",
"email": "ada@example.com",
}
]
},
}
],
}
],
}
response = requests.post(
f"{API_BASE}/accounts/{ACCOUNT_ID}/envelopes",
headers={
"Authorization": f"Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
},
data=json.dumps(envelope),
)
response.raise_for_status()
print(response.json()["envelopeId"])
```
生产环境把演示域名换成 https://www.docusign.net,访问令牌通过你惯用的 OAuth 流程获取,并且把凭据隔离在代码仓库之外——上面的占位符存在是有原因的。
envelope 完成后,集成通常还有下游工作:取回签署人实际填写的内容。从已签署的 DocuSign 文档中获取控件数据和表单字段的做法(通过 recipients 与 envelope-form-data 端点)与这套发送流程天然配套;下载合并为单个 PDF 的完成证书存入合规档案也是如此。
组合模板 vs 单一 templateId:如何选型
组合模板会增加 JSON 复杂度,所以动手之前,先确认你的场景确实需要它。这张决策表覆盖了常见情形:
还有一个相关的成本考量:重度使用 envelope 的集成,恰恰是 API 速率限制和账户档位开始发挥影响的那类负载。做规模化架构设计时,建议把这篇 DocuSign 与 Dropbox Sign API 在速率限制与定价档位上的对比和 DocuSign 官方的限制文档放在一起读。
常见组合模板错误排错清单
当请求失败或生成了奇怪的 envelope 时,按顺序过一遍这份清单:
- [ ]
documentId重复。 最终 envelope 中每份文档的documentId必须唯一。如果你的运行时文档撞上了一个你并不打算替换的模板文档,API 会报 ID 已被占用的错误。有意识地分配未占用的 ID。 - [ ] 替换没有发生——envelope 里同时存在两份文档。 你上传文档的
documentId没有匹配到所引用模板内的任何文档。确认模板内部的文档 ID(模板的 JSON 或 DocuSign 网页界面中都能看到),并复用那个精确的值。 - [ ] 替换后控件落点错误。 固定位置的控件会把
xPos/yPos原样带到新文件上。如果你生成的文档布局与模板原文件不同,把这些控件改成基于anchorString的定位,或在文本可能不存在时设置anchorIgnoreIfNotPresent。 - [ ] sequence 错误。 同一组合模板内
serverTemplates与inlineTemplates的sequence值必须唯一(DocuSign 文档将其描述为定义处理顺序)。重复的值通常会触发一条指明问题项的校验错误。 - [ ] 签署人合并失败。 内联签署人按
roleName与模板签署人匹配。角色名里的一个拼写错误会让模板的占位签署人悬空,在发送时表现为未分配签署人或 envelope 未发送的错误。 - [ ]
document与documents混用。 每个组合模板二选一。如果一个组合模板里需要多份运行时文档,把它们全部列进documents,不要再写document。 - [ ] envelope 卡在
created状态。status必须是"sent"才会立即给签署人发邮件;"created"是刻意构建草稿。在断定投递失败之前,先检查你的代码设置的是哪一个。
一点提醒:DocuSign 的校验逻辑和错误文案会随 API 版本与账户配置变化,请把这份清单当作分诊指南;当实际报错与清单描述对不上时,以官方 createEnvelope 参考中的确切错误文案为准。
告别 JSON 装配流水线:Nota Sign 的动态文档方案
组合模板解决的是真实存在的问题,但它也展示了动态文档要付出多少仪式成本:Base64 负载、sequence 算术、角色名耦合、documentId 记账——仅仅为了把一份生成的 PDF 和一份模板合并在一起。如果你的待办列表正被 envelope 组装胶水代码填满,在这些胶水凝固成架构之前,值得评估一下 Nota Sign——法大大旗下的全球电子签名平台——作为文档密集型签署流程的 API 底座。
API 之下的平台是为规模化生产而建的:法大大连续多年被 IDC 评为中国电子签名软件市场第一,法律覆盖横跨 100 多个国家和地区;亚太部署可接入 iAM Smart 与 Singpass,支持 SES/AES/QES 签名等级,并以区域数据中心满足数据驻留要求。定价同样遵循低摩擦理念:构建集成的工程师和运营人员不按席位收费,方案按你实际预估的业务量定制。
描述你的场景——生成式文档、模板、跨境签署人——直接问 API 如何应对:联系 Nota Sign。








