资讯动态

DocuSeal Submission Webhook 深度解析:四类事件的触发链路、载荷结构与投递保障机制

发布时间:2026/9/13 18:23:27 来源:尧图企业网站定制
DocuSeal Submission Webhook 深度解析四类事件的触发链路、载荷结构与投递保障机制【免费下载链接】docusealOpen source DocuSign alternative. Create, fill, and sign digital documents ✍️项目地址: https://gitcode.com/GitHub_Trending/do/docusealDocuSeal 的 Webhook 机制允许你在文档提交Submission的生命周期节点——创建、完成、过期、归档——实时收到 HTTP 回调通知。本文基于仓库中的 submission-webhook.md 文档结合 lib/send_webhook_request.rb、lib/webhook_urls.rb 等核心源码完整讲解四类 submission 事件的触发链路、回调请求格式、HMAC 签名验证方式、载荷字段的真实来源以及内置的重试与投递记录机制。一、四类 submission 事件及其含义官方文档定义了四个提交级事件事件类型触发时机submission.created提交submission被创建时submission.completed所有签署方均完成签署时submission.expired提交过期时submission.archived提交被归档时这四个事件在源码中一一对应到独立的 Sidekiq Job映射关系定义在 lib/webhook_urls.rb 的EVENT_TYPE_TO_JOB_CLASS中EVENT_TYPE_TO_JOB_CLASS { form.started SendFormStartedWebhookRequestJob, form.completed SendFormCompletedWebhookRequestJob, form.declined SendFormDeclinedWebhookRequestJob, form.viewed SendFormViewedWebhookRequestJob, submission.created SendSubmissionCreatedWebhookRequestJob, submission.completed SendSubmissionCompletedWebhookRequestJob, submission.expired SendSubmissionExpiredWebhookRequestJob, submission.archived SendSubmissionArchivedWebhookRequestJob, # ...template 系列事件 }.freeze也就是说DocuSeal 同时提供三类事件域form.*单个签署人表单维度、submission.*整个提交维度本文主题与template.*模板维度你可以在同一个或不同 Webhook 端点上按需订阅。二、事件触发与入队链路当业务动作发生时例如通过 API 创建提交、Web 端开始签署、签署方完成、提交被归档等系统调用WebhookUrls.enqueue_events(records, event_type)。以 API 创建提交为例app/controllers/api/submissions_controller.rb 中有WebhookUrls.enqueue_events(submissions, submission.created)其他已确认的触发点包括Web 表单入口app/controllers/start_form_controller.rb 在签署流程启动时为对应 submission 入队submission.created提交归档app/controllers/submissions_controller.rb 与 API 侧 submissions_controller.rb 在归档时入队submission.archived完成事件app/jobs/process_submitter_completion_job.rb 在签署人完成处理后入队submission.completed过期事件由过期处理 Job app/jobs/process_submission_expired_job.rb 所在流程负责从源码结构看与其余事件遵循相同的入队模式。enqueue_events的核心逻辑lib/webhook_urls.rbdef enqueue_events(records, event_type) args [] id_key EVENT_TYPE_ID_KEYS.fetch(event_type.split(.).first) # submission submission_id Array.wrap(records).group_by(:account_id).each do |account_id, account_records| webhook_urls for_account_id(account_id, event_type) account_records.each do |record| event_uuid SecureRandom.uuid webhook_urls.each do |webhook_url| next unless webhook_url.events.include?(event_type) args [{ id_key record.id, webhook_url_id webhook_url.id, event_uuid event_uuid }] end end end Sidekiq::Client.push_bulk(class EVENT_TYPE_TO_JOB_CLASS[event_type], args args) end两个关键设计值得注意事件去重 ID每次业务动作只生成一个event_uuid该提交对应的所有 Webhook 端点共享同一个 UUID。结合投递记录表见第六节系统可以识别该事件已向此端点成功投递过避免重试时重复发送。批量入队Sidekiq::Client.push_bulk将多个(记录, 端点)组合一次性推入 Sidekiq全部使用:webhooks队列见各 Job 的sidekiq_options queue: :webhooks如 send_submission_created_webhook_request_job.rb与业务请求解耦。三、回调请求的实际格式每个 Job以 SendSubmissionCreatedWebhookRequestJob 为例执行流程固定按submission_id与webhook_url_id取出记录校验端点 URL 非空且订阅了当前事件webhook_url.events.exclude?(submission.created)则直接返回调用SendWebhookRequest.call载荷由Submissions::SerializeForApi.call(submission)生成响应状态码 400 时按指数退避重新调度自己。真正的 HTTP 请求由 lib/send_webhook_request.rb 通过 Faraday 发出要点如下response Faraday.post(uri) do |req| req.headers[Content-Type] application/json req.headers[User-Agent] USER_AGENT # DocuSeal.com Webhook req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present? # 自定义请求头 req.body { event_type: event_type, # 如 submission.completed timestamp: webhook_event.created_at || Time.current, data: data # 下文第四节详述 }.to_json if req.headers[X-Docuseal-Signature].blank? req.headers[X-Docuseal-Signature] WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: req.body) end req.options.read_timeout 15 # 读取超时 15 秒 req.options.open_timeout 8 # 连接超时 8 秒 end请求特征总结方法/格式POSTContent-Type: application/jsonUser-Agent固定为DocuSeal.com Webhook可用于接收端来源识别签名头X-Docuseal-SignatureHMAC-SHA256第五节详述自定义头端点上配置的secret一个 JSON 对象会合并进请求头可用于携带静态鉴权信息超时连接 8 秒 / 读取 15 秒超时按连接失败处理并进入重试安全约束在多租户部署模式下DocuSeal.multitenant?send_webhook_request.rb 强制要求https且端口为 443除非账户配置了allow_http并禁止向 localhost 类地址发送。四、载荷结构data 字段完整解读回调 body 顶层为{ event_type, timestamp, data }其中data是提交对象。以下按 submission-webhook.md 文档 schema 逐字段说明并标注其在源码中的实际来源序列化逻辑位于 lib/submissions/serialize_for_api.rb4.1 提交主体字段字段类型说明idnumber提交唯一标识namestring文档提交名称slugstring提交唯一 slugexpire_atstring | null提交过期时间archived_atstring | null归档时间created_at/updated_atstring创建 / 更新时间sourcestring提交来源枚举invite、bulk、api、embed、linksubmitters_orderstring签署人顺序枚举random、preservedaudit_log_urlstring | null审计日志文件 URL仅完成后可用combined_document_urlstring | null含文档与审计日志的合并 PDF URLstatusstring提交状态枚举completed、declined、expired、pendingcompleted_atstring提交整体完成时间variablesobject动态内容变量对象documentsarray顶层文档列表nameurl完成前为空数组templateobject基础模板信息id、name、external_id、folder_name、created_at、updated_atcreated_by_userobject创建者信息id、first_name、last_name、emailsubmission_eventsarray该提交的事件流水见 4.4状态机逻辑可以直接在源码中验证serialize_for_api.rb若submission.completed_at?为真则status置为completed并填充audit_log_url与combined_document_url取自最后完成签署的那位签署人生成的文档否则按是否存在拒签签署人 →declined是否已过期 →expired其余pending的顺序判定。4.2 submitters 数组data.submitters是签署人列表每个元素包含字段类型说明id/submission_idnumber签署人 / 所属提交 IDuuidstring签署人 UUIDemailstring签署人邮箱slugstring唯一 slugsent_at/opened_at/completed_at/declined_atstring | null发送 / 打开 / 完成 / 拒签时间戳created_at/updated_atstring创建 / 更新时间name/phone/rolestring | null姓名、E.164 手机号、角色名如 First Partyexternal_idstring | null你的应用中用于标识该签署人的自定义键metadataobject附加元数据statusstring签署人状态枚举completed、declined、opened、sent、awaitingvaluesobject预填值对象字段名为 keydocumentsarray该签署人的文档列表nameurlpreferencesobject签署人偏好4.3 template 与 created_by_userdata.template提供模板维度信息id、name、external_id你在应用内标识模板的自定义键、folder_name、created_at、updated_at。data.created_by_user提供创建该提交的用户id、first_name、last_name、email。这两块对应的 include 关系显式声明在序列化参数中serialize_for_api.rbSERIALIZE_PARAMS { only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at], include: { submitters: { only: %i[id] }, template: { only: %i[id name external_id created_at updated_at], methods: %i[folder_name] }, created_by_user: { only: %i[id email first_name last_name] } } }.freeze4.4 submission_events 事件流水data.submission_events记录提交的完整操作流水每个事件包含id、submitter_id、event_type、event_timestamp与data附加事件详情。event_type的完整枚举为send_email, bounce_email, complaint_email, send_reminder_email, send_sms, send_2fa_sms, open_email, click_email, click_sms, phone_verified, start_form, start_verification, complete_verification, view_form, invite_party, complete_form, decline_form, api_complete_form它覆盖邮件收发/打开/点击、短信、表单开始/查看/完成/拒绝、API 完成等全链路动作适合做审计回溯。4.5 一个实际的载荷示例综合以上结构submission.completed事件的 body 大致形如{ event_type: submission.completed, timestamp: 2023-09-24T11:20:42Z, data: { id: 1024, name: NDA - Acme Corp, slug: a1b2c3, expire_at: null, archived_at: null, created_at: 2023-09-01T10:00:00Z, updated_at: 2023-09-24T11:20:42Z, source: api, submitters_order: preserved, variables: {}, status: completed, completed_at: 2023-09-24T11:20:42Z, audit_log_url: https://docuseal-host/rails/active_storage/blobs/.../audit_log.pdf, combined_document_url: https://docuseal-host/rails/active_storage/blobs/.../combined.pdf, documents: [ { name: audit_log.pdf, url: https://docuseal-host/... }, { name: combined.pdf, url: https://docuseal-host/... } ], submitters: [ { id: 56, submission_id: 1024, uuid: 9f3c..., email: john.doeexample.com, slug: s1, sent_at: 2023-09-01T10:00:05Z, opened_at: 2023-09-10T09:00:00Z, completed_at: 2023-09-24T11:20:41Z, declined_at: null, status: completed, external_id: customer_42, values: {}, documents: [], preferences: {} } ], template: { id: 33, name: NDA Template, external_id: nda-2023, folder_name: Contracts, created_at: 2023-05-15T00:00:00Z, updated_at: 2023-08-01T00:00:00Z }, created_by_user: { id: 1, first_name: Jane, last_name: Doe, email: janeexample.com }, submission_events: [] } }注意一个事件粒度差异submission.archived的载荷是精简版——从 SendSubmissionArchivedWebhookRequestJob 可以看到它仅发送data: submission.as_json(only: %i[id archived_at])即只包含id与archived_at因为归档后完整数据不再有意义。其余三个事件的data均为完整的Submissions::SerializeForApi序列化结果。五、签名与验签机制DocuSeal 为每个 Webhook 端点自动生成一个 HMAC 密钥24 字节随机值Base64 编码前缀whsec_见 app/models/webhook_url.rb 的set_hmac_secret与 lib/webhook_urls/signatures.rb 的generate_secret。签名算法signatures.rbdef sign(secret, body:, timestamp: Time.current.to_i) #{timestamp}.#{OpenSSL::HMAC.hexdigest(sha256, secret, #{timestamp}.#{body})} end即X-Docuseal-Signature: unix秒级时间戳.hex(HMAC-SHA256(secret, 时间戳.原始body))。接收端验证逻辑与 verify 完全对齐要点以.拆分头值取时间戳ts与签名sig时间戳容差为 5 分钟TOLERANCE 5 * 60过旧ts now - 300或超前于当前时间均抛出TimestampError用于防重放用相同算法计算期望签名并用ActiveSupport::SecurityUtils.secure_compare做恒定时间比较防时序侧信道。接收端伪代码# secret 即端点的 whsec_ 密钥 ts, sig header_value.split(., 2) raise unless ts.to_i.between?(Time.current.to_i - 300, Time.current.to_i 300) expected OpenSSL::HMAC.hexdigest(sha256, secret, #{ts}.#{raw_body}) raise unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)端点模型还值得注意两点url、secret、hmac_secret三个字段均通过 Rails 的encrypts做字段级加密存储webhook_url.rb并且模型内置了全部 11 种可订阅事件白名单EVENTSwebhook_url.rbsubmission.*四个事件均在其中默认订阅事件为四个form.*事件。六、重试策略与投递记录6.1 指数退避重试四个 Job 的重试骨架完全一致以 send_submission_completed_webhook_request_job.rb 为例return if attempt MAX_ATTEMPTS || (resp resp.status.to_i 400) # MAX_ATTEMPTS 10 SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, { **params, attempt attempt 1, last_status resp.status.to_i })最多 10 次自动重试MAX_ATTEMPTS 10退避间隔为2^attempt分钟第 1 次失败后 2 分钟、第 2 次后 4 分钟、第 3 次后 8 分钟……间隔指数增长只要响应状态码 400含 2xx/3xx即视为成功不再重试网络层异常Faraday::SSLError、Faraday::TimeoutError、Faraday::ConnectionFailed等同样记录为错误并触发重试send_webhook_request.rb。6.2 投递状态与防重复每次发送都会在webhook_events表写入/更新一条记录WebhookEvent状态pending → success/error并为每次尝试追加一条WebhookAttempt含response_status_code、截断至 100 字符的response_body与attempt序号见 handle_response / handle_error。对应的数据表由 20250627130628_create_webhook_events_and_attempts.rb 迁移创建。防重复机制位于 send_webhook_request.rbevent_uuid与webhook_url共同唯一标识一条WebhookEvent若该事件此前已成功投递status success自动重试到达时会直接跳过发送。这保证了同一业务事件在同一端点上至多投递一次成功载荷。管理端可以通过 Webhook 设置页面查看投递记录webhook_events_controller.rb对应的行为在 spec/requests/ 与 spec/jobs/ 下均有覆盖例如 send_submission_completed_webhook_request_job_spec.rb 与 send_submission_archived_webhook_request_job_spec.rb 分别验证了完成与归档事件的发送与重试行为。七、接收端接入要点清单结合以上源码事实接收端工程实现时建议幂等处理以data.id提交 IDevent_type做幂等键重试场景下同一事件可能多次到达成功投递后依赖端点侧去重先验签再处理校验X-Docuseal-Signature5 分钟时间戳容差 恒定时间比较验签失败直接拒绝快速返回服务端连接/读取超时分别为 8 秒 / 15 秒接收端应尽快返回 2xx重活丢给自己的队列区分事件粒度submission.archived的data只有id与archived_at不要按完整 schema 解析来源识别User-Agent固定为DocuSeal.com Webhook可辅助与自身其他回调区分多租户部署注意HTTPS 强制 localhost 禁发规则仅作用于多租户模式自托管私有部署可放宽源码中由Docuseal.multitenant?分支控制。八、参考文件索引关注点文件事件与载荷文档docs/webhooks/submission-webhook.mdHTTP 发送与重试lib/send_webhook_request.rb签名/验签lib/webhook_urls/signatures.rb事件→Job 映射与入队lib/webhook_urls.rb端点模型事件白名单、字段加密app/models/webhook_url.rb四个 submission 事件 Jobcreated / completed / expired / archived载荷序列化lib/submissions/serialize_for_api.rb投递记录迁移db/migrate/20250627130628_create_webhook_events_and_attempts.rb相关 Job 测试spec/jobs/ 目录下各send_submission_*_webhook_request_job_spec.rb【免费下载链接】docusealOpen source DocuSign alternative. Create, fill, and sign digital documents ✍️项目地址: https://gitcode.com/GitHub_Trending/do/docuseal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价