用 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。








