跳转至主要内容
Certyneo
开发人员的文档

网络网络即时收到签名活动

在您的 Certyneo 仪表板中配置 HTTPS URL,每当您的信封上发生事件时(签名、拒绝、过期),即可接收 HMAC-SHA256 签名的 POST。支持 11 个事件,5 次指数退避传递尝试,密码验证只需 8 行代码。

< 5s

事件后的平均交付时间

5x

总传递尝试次数,分散在大约 1 小时 20 分钟内

HMAC-SHA256

每个请求的签名算法

活动目录

下面的 12 个事件涵盖了 Certyneo 信封生命周期的全部内容。在设置 → Webhooks 中启用您感兴趣的事件,忽略其他事件——订阅按事件粒度划分。

活动发作
envelope.created创建一个包裹 (通过UI,API或模板) 很有用,可以在创建时同步CRM侧记录.
envelope.sent封信发送给签名者 (首次发送电子邮件).标志着活跃签名循环的开始.
envelope.completed所有签署人都已签署,密封的 eIDAS PDF 已存储。负载包含 signedDocumentUrl,这是一个有效期为 7 天的预签名链接;否则,使用 GET /v1/envelopes/id/signed-document,以及通过 GET /v1/envelopes/id/audit-trail 获取审计日志。
envelope.declined签署人拒绝了信封。拒绝人的地址在 `data.declinedBy` 中,如果输入了原因,则在 `data.reason` 中。
envelope.voided封面被发行人取消了,直到签名完成.
envelope.expired信封的过期日期已过,但未完成签署。查询 GET /v1/envelopes/id 以了解缺失的签署人。
envelope.returned_to_sender签署人将信封返回给发行人进行更正,但未拒绝该信封。原因在 `data.reason` 中,返回人在 `data.returnedBy` 中。
envelope.resubmitted发行人进行了更正,然后重新发送了之前退回的信封。标志着签名周期的恢复。
recipient.signed单个签署人已签署(但不一定是全部)。有助于跟踪进度并进入顺序工作流的下一步。注意:即使对于最后一个签署人,此阶段也不存在密封的 PDF——从此处启动的下载会返回 HTTP 409。请使用 envelope.completed 来获取文件。
recipient.viewed一位签名者在未签署之前打开了签名链接,这对于有针对性的商业启动很有用.
recipient.approved批准人验证了信封但未在其上签署(内部验证工作流)。地址在 `data.approvedBy` 中。
recipient.bounced收件人的邮件服务器已永久拒绝邀请或重试(邮箱不存在、域名已停用)。收件人的状态变为 BOUNCED,自动重试将停止。该地址在 `data.recipientEmail` 中:请更正后重新发送信封。

recipient.signed 并不意味着「文件可用」

recipient.signed 对于每个签署人都会发出,在他们完成签署时发出——包括最后一个签署人,在密封的 PDF 被组装和存储之前。从此处理程序触发的下载因此总是会收到 HTTP 409「Signed document not available until the envelope is COMPLETED」。这不是错误:这是「还没准备好」。订阅 envelope.completed 以检索文件,并保留 recipient.signed 以跟踪进度(谁签署了,何时签署)。

有效载荷的格式

所有交付共享相同的顶级 JSON 架构:`event`、`data` 和 `timestamp`。事件名称也在 `X-Certyneo-Event` 标头中重复,这允许在解析请求体之前进行路由。`data` 的内容因事件而异,但始终是简单值的平面对象——永远没有数组或嵌套对象。以下是完整的 `envelope.completed` 交付。

POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c
{
  "id": "cm8f2h6a10004qr9k5p2wm3xt",
  "event": "envelope.completed",
  "data": {
    "envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
    "sandbox": false,
    "subject": "Contrat de prestation Acme Corp",
    "status": "COMPLETED",
    "signatureLevel": "ADVANCED",
    "aesSignerCount": 2,
    "completedAt": "2026-05-27T08:42:13.000Z",
    "recipientCount": 2,
    "recipients": [
      { "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
      { "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
    ],
    "fieldValues": [
      { "fieldId": "cm7x2k9p40005qz8h1a2b3c4d", "fieldType": "RADIO_GROUP", "recipientEmail": "alice@acme.com", "value": "Option B" }
    ],
    "signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
  },
  "timestamp": "2026-05-27T08:42:13.521Z"
}

signedDocumentUrl 是一个从事件开始有效期为 7 天的预签名链接:它避免了第二次身份验证调用来检索 PDF。如果预签名失败,或在没有存储文件的已完成 QES 信封上,则不存在该链接——而不是 null;此时请重新使用 GET /v1/envelopes/id/signed-document,这仍然是真实来源。还要注意,在事件发生 7 天后从死信队列手动重放:负载中的链接已过期,API 端点没有过期。

收件符号是UTF-8编码,没有BOM.HMAC签名计算在原始体内,如发送不要改变空格,JSON重新解析经常改变密钥的顺序,破坏验证.

检查HMAC的签名

每个请求都使用您的webhook密钥签名(在创建订阅时仅显示一次)。签名在`X-Certyneo-Signature`请求头中传输:这是请求原始正文的HMAC-SHA256,以十六进制编码,不带前缀或时间戳。处理payload前请ALWAYS验证签名——没有这一步骤,任何人都可以伪造事件并调用您的端点。

Node.js / TypeScript

import crypto from "node:crypto";

export function verifyCertyneoSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  // X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signatureHeader, "hex");
  const computed = Buffer.from(expected, "hex");

  // timingSafeEqual throws when the two buffers differ in length —
  // a malformed header must return false, not crash the handler.
  if (received.length !== computed.length) return false;

  // timingSafeEqual to mitigate timing attacks.
  return crypto.timingSafeEqual(computed, received);
}

Python

import hashlib
import hmac


def verify_certyneo_signature(
    raw_body: bytes, signature_header: str, secret: str
) -> bool:
    """Verify a Certyneo webhook signature.

    `X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
    raw request body, computed with your webhook secret.
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature_header)

常见的错误

不要使用 `===`或 `==`来比较预期和接收的签名.使用一个时间安全函数 (`crypto.timingSafeEqual`在Node中, `hmac.compare_digest`在Python中).否则,两个签名之间的比较时间差距会逐渐向耐心的攻击者揭示秘密 (时间攻击).

试用政策

如果您的端点响应耗时过长、拒绝连接或返回5xx(或429),我们将按指数退避重试:总共5次尝试,耗时约1小时20分。第五次后,事件进入死信队列——可从您的仪表板查询和重放,但不再自动重试。明确拒绝(401、403、404、410、422…)则从不重试:响应不会改变,事件直接进入死信队列。

试图重试前的延迟事件发生后经过的时间
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 15 min21 min
#5+ 1 h1 h 21

指定的延迟时间是最小值:重试扫描由cron控制,因此重试可能会在理论时间之后稍微才开始。放弃的事件列在Webhooks → 失败中,带有手动重放按钮且无保留期限。连续失败五次的端点——或响应404/410的端点——会自动停用,您将收到电子邮件通知:修正后重新激活,计数器重置为零(任何成功交付也会将其重置为零)。

试验没有发送真正的信封

首先创建一个订阅——响应包含HMAC验证所需的`secret`,仅显示一次。从设置 → Webhooks,「测试」按钮随后会发送一个签名的POST,完全像真实交付一样,并显示您服务器的原始响应:这是验证您的处理程序的最快方式,通过ngrok在本地或在持续集成中进行。

# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
  -H "Authorization: Bearer $CERTYNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your.app/webhooks/certyneo",
    "events": ["envelope.completed", "recipient.signed"]
  }'

URL必须在HTTPS上公开可访问:私有地址或localhost会在创建订阅时被SSRF保护拒绝。

订阅按环境隔离,就像密钥一样:使用 sk_test_ 密钥创建的 webhook 仅接收测试信封的事件,使用 sk_live_ 密钥创建的 webhook 仅接收真实信封的事件。因此,测试信封永远不会到达您的生产 URL,每个负载都指示"sandbox"(true 或 false)。

六个需要遵守的做法

  • 检查HMAC签名在读取任何体内之前 使用时间安全的比较.
  • 在`X-Certyneo-Delivery-Id`请求头上进行去重(与请求体中的`id`相同),通过在数据库中存储已见过的标识符——如果您的2xx响应丢失了,重放可能会重新发送您已处理过的事件,每次尝试时使用相同的标识符。
  • 在最多10秒内响应HTTP 2xx,然后异步处理(队列)。超过此时间,交付将被切断并计为失败。
  • ,HMAC检查通常会失败,因为BOM或空白空间是看不见的.
  • 监控 Webhooks → 失败页面:如果您的端点被禁用,您会收到电子邮件通知,但不会逐个事件通知——如果事件用尽了重试次数,您需要手动重新播放该事件。
  • 在订阅时过滤事件而不是在您的处理程序中过滤,并保持在每个账户 5 个订阅的限制以下。

您不需要托管一个端点来接收这些事件:连接器会为您设置订阅,并直接在信封事件上启动流程。查看 Certyneo for Power Automate 和 Microsoft 365.

Power Automate:订阅自动管理

使用「当信封事件发生时」触发器,无需复制或更新任何地址。Power Automate 会使用您创建的连接自动在 Certyneo 注册订阅。

  • 您在触发器卡中选择事件,然后保存并激活流:此时会创建订阅。
  • 当您停用或删除流时,Power Automate 会自动删除订阅。
  • 连接决定哪个账户(及哪个工作区)发送事件。在生产环境中,使用与正确账户相关联的连接。
  • 如果您修改触发器(例如更改事件),请再次保存流:订阅会更新。
  • 如果流不再触发,请停用它,然后重新激活:订阅会重新创建。

从开发环境迁移到生产环境

  1. 将您的解决方案导入生产环境。
  2. 打开流并将其关联到生产环境中要使用的 Certyneo 连接,该连接需用实际密钥(sk_live_)创建。
  3. 激活流。新的生产订阅被创建:无需在 Certyneo 端配置任何内容,也无需修改任何地址。
  4. 进行测试:签署或完成信封,然后检查流历史中是否显示执行记录。

为了更进一步

准备好连接系统吗?

Webhooks 和 REST API 包含在标准计划及以上版本中。创建您的账户,生成一个 `sk_test_` 密钥,并在沙箱环境中连接您的端点,然后再进入生产环境。