DocuSign API 在 Production 创建 Envelope 报错
结论
当前请求有两个问题需要修正:
-
documentBase64中保存的不是 PDF 文件的 Base64,而是模板元数据 JSON 的 Base64。请求却将fileExtension声明为"pdf"。DocuSign 尝试按 PDF 解析这段内容时会失败,并返回:{ "errorCode": "UNSPECIFIED_ERROR", "message": "Input string was not in a correct format." } -
Production 请求地址不应手动固定为
https://na2.docusign.net:8821/...。应从认证或 UserInfo 响应中获取目标账户对应的base_uri,再拼接 REST API 路径。8821很可能是 DocuSign 服务端日志中的内部端口,不能作为客户端配置依据。
问题与 embedded signing、Ruby HTTP 库或 tabs 数量无关。精简 payload 后仍然失败,是因为其中的文档内容依然无效。
为什么可以确定 documentBase64 不是 PDF
示例中的 Base64 以以下内容开头:
ew0KICAidGVtcGxhdGVJZCI6...
解码后得到的是:
{
"templateId": "1b6ccad1-5d1f-403a-b4a0-3e5df5598ae5",
"templateDocuments": [
{
"documentId": "44202387",
"name": "test.pdf",
"type": "content",
"uri": "/templates/1b6ccad1-5d1f-403a-b4a0-3e5df5598ae5/documents/44202387",
"order": "1",
"pages": "1"
}
]
}
这段 JSON 只是对模板所含文档的描述,并不是 test.pdf 的二进制内容。
真正的 PDF 经 Base64 解码后,通常以 PDF 文件签名开头:
%PDF-
对应的 Base64 一般以类似下面的内容开头:
JVBERi0
Base64 字符串格式合法,不代表它就是有效的 PDF。Base64 只是一种编码方式,JSON、PDF、图片和普通文本都可以编码成 Base64。
正确获取并编码文档
如果文档来自本地文件,应读取文件的原始二进制内容,再进行编码:
require "base64"
pdf_bytes = File.binread("test.pdf")
document_base64 = Base64.strict_encode64(pdf_bytes)
不要对以下内容进行 Base64 编码:
- 模板详情响应
- 模板文档列表响应
- 包含
templateId、uri等字段的 JSON - 已经编码为 Base64 的字符串
- HTTP response 对象的字符串表示
如果文件来自 DocuSign 模板,需要调用实际的模板文档下载接口,取得响应 body 中的二进制文件。不要使用模板文档列表接口返回的 JSON。请求形式如下:
GET {base_uri}/restapi/v2.1/accounts/{accountId}/templates/{templateId}/documents/{documentId}
Authorization: Bearer {access_token}
Accept: application/pdf
然后直接编码返回的二进制 body:
pdf_bytes = response.body
document_base64 = Base64.strict_encode64(pdf_bytes)
编码前可以检查文件签名:
unless pdf_bytes.start_with?("%PDF-")
raise "Downloaded content is not a PDF"
end
发送前也可以解码一次,确认 documentBase64 中确实保存了 PDF:
decoded = Base64.strict_decode64(document_base64)
unless decoded.start_with?("%PDF-")
raise "documentBase64 does not contain PDF bytes"
end
使用正确的 Production endpoint
不要将 Sandbox 地址替换成自行猜测的 Production 集群地址,也不要固定使用 na2。Production 账户可能位于不同站点。
应读取 OAuth UserInfo 返回的账户信息,并使用目标账户对应的 base_uri:
{
"accounts": [
{
"account_id": "15308603",
"base_uri": "https://na2.docusign.net"
}
]
}
据此构造请求地址:
https://na2.docusign.net/restapi/v2.1/accounts/15308603/envelopes
不要主动添加 :8821:
https://na2.docusign.net:8821/restapi/...
如果 :8821 只出现在 DocuSign 提供的内部服务器日志中,并不能据此判断客户端实际配置了这个端口。应检查客户端最终发送的 URL,并以认证响应中的 base_uri 为准。
最小可用请求示例
先去掉 anchors、tabs、carbon copies 和 embedded signing,使用经过验证的 PDF 进行最小化测试:
require "base64"
require "json"
require "net/http"
require "uri"
account_id = "15308603"
base_uri = "https://na2.docusign.net"
access_token = ENV.fetch("DOCUSIGN_ACCESS_TOKEN")
pdf_bytes = File.binread("test.pdf")
raise "Invalid PDF" unless pdf_bytes.start_with?("%PDF-")
payload = {
emailSubject: "DocuSign REST API test",
documents: [
{
documentBase64: Base64.strict_encode64(pdf_bytes),
name: "test.pdf",
fileExtension: "pdf",
documentId: "1"
}
],
recipients: {
signers: [
{
email: "signer@example.com",
name: "Test Signer",
recipientId: "1",
routingOrder: "1"
}
]
},
status: "sent"
}
uri = URI(
"#{base_uri}/restapi/v2.1/accounts/#{account_id}/envelopes"
)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{access_token}"
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate(payload)
response = Net::HTTP.start(
uri.hostname,
uri.port,
use_ssl: true
) do |http|
http.request(request)
end
puts response.code
puts response.body
documentId 可以先使用简单的 "1"。它是当前 envelope 内用于关联文档和 tabs 的标识,不必沿用模板中较大的文档 ID。添加 tabs 后,每个 tab 的 documentId 必须与 envelope 中的文档 ID 一致:
{
"documents": [
{
"documentId": "1"
}
],
"tabs": {
"signHereTabs": [
{
"documentId": "1",
"recipientId": "1",
"anchorString": "signer1sig"
}
]
}
}
如果目的是从模板创建 Envelope
如果 Production 中已有可用模板,就不必先下载模板文档、重新编码,再将其作为新文档上传。可以直接通过 templateId 创建 envelope:
{
"templateId": "PRODUCTION_TEMPLATE_ID",
"templateRoles": [
{
"roleName": "Customer",
"name": "Mic Nuss",
"email": "myemail@gmail.com",
"clientUserId": "an-alpha-numeric-ID"
}
],
"status": "sent"
}
需要注意:
- Sandbox 和 Production 是两个独立环境,
templateId通常不能混用。 - Production 请求必须使用在 Production 中实际创建或导入后生成的模板 ID。
roleName必须与 Production 模板中的角色名称完全一致。- 直接使用模板时,不要同时提交从模板元数据中提取的伪
documentBase64。
Production 侧还应检查的项目
修正文档内容和 endpoint 后,如果请求仍然失败,可以依次检查:
- OAuth UserInfo 中的
account_id是否与 URL 中的{accountId}完全一致。 - 使用的是否为该账户对应的
base_uri,而不是默认账户或 Sandbox 的地址。 - Integrator Key 是否已完成 Production go-live,以及当前认证方式是否被 Production 接受。
- Production 账户是否具备发送 envelope 的权限,当前用户是否可以创建和发送 envelope。
- 请求体是否由 JSON 序列化器生成,且没有手工拼接或二次编码。
Content-Type是否为application/json。- PDF 是否为原始二进制文件的 Base64,而不是 JSON、下载页面或错误响应的 Base64。
- 使用模板时,是否采用了 Production 的
templateId和对应角色。
最直接的修复方法是使用一个本地可以正常打开的 PDF,确认其二进制内容以 %PDF- 开头,通过 Base64.strict_encode64 编码,并以 documentId: "1" 发送到 UserInfo 返回的 Production base_uri。最小请求成功后,再逐步恢复模板、tabs、carbon copies 和 embedded signing 配置。
备注:内容仅供参考。
