Docuseal Webhook Events: Available Types and Event Processing Pipeline

Docuseal exposes three webhook families—Template, Submission, and Form—that deliver HMAC-signed JSON payloads via a deterministic, retry-aware background job pipeline whenever specific document lifecycle changes occur.

Docuseal implements a robust webhook system that notifies external systems about template modifications, form submissions, and user interactions. The platform organizes these events into three distinct families, each processed through a standardized six-stage pipeline defined in the docusealco/docuseal repository. Understanding these available webhooks and their event processing architecture is essential for building secure, reliable integrations.

Available Webhook Families and Events

Docuseal organizes webhook events into three families based on the underlying domain model. Each family fires specific events implemented as dedicated background jobs.

Template Events

Template webhooks fire when document templates change state. According to app/jobs/send_template_created_webhook_request_job.rb and related job files, the following events are available:

  • template.created — Triggered after a new template is persisted to the database via the after_create_commit callback
  • template.updated — Fired when template attributes such as name, fields, or settings are modified
  • template.archived — Occurs when a template is soft-deleted or moved to archived status

Submission Events

Submission events track the lifecycle of filled and signed documents. The app/jobs/send_submission_created_webhook_request_job.rb handles creation events, while sibling jobs manage other state transitions:

  • submission.created — Dispatched when a user completes and saves a form submission
  • submission.completed — Fired when all required fields are filled and the submission is finalized and locked
  • submission.expired — Triggered when a submission reaches its configured expiration date without completion
  • submission.archived — Occurs when a submission is archived after signing or administrative action

Form Events

Form events capture user interactions with the document filling interface. Jobs such as app/jobs/send_form_viewed_webhook_request_job.rb process these interaction events:

  • form.viewed — Triggered when a form URL is loaded (initial page view)
  • form.started — Fired when the user begins interacting with form fields (first input)
  • form.declined — Occurs when a user explicitly declines or abandons the form without completing
  • form.completed — Dispatched when the user successfully finishes the form flow, which may or may not create a submission

How Webhook Events Are Processed

The platform processes webhook events through a deterministic six-stage pipeline implemented across models, jobs, and service classes.

1. Event Detection and Job Enqueuing

Model callback hooks detect state changes and enqueue specific background jobs. In app/models/submission.rb, an after_create_commit callback triggers the delivery pipeline:

after_create_commit do
  SendSubmissionCreatedWebhookRequestJob.perform_later(id)
end

Each event type maintains an isolated job class—such as SendTemplateCreatedWebhookRequestJob and SendFormViewedWebhookRequestJob—ensuring independent processing queues and failure domains.

2. Payload Serialization

The Submitters::SerializeForWebhook service class transforms model instances into JSON structures. Located in lib/submitters/serialize_for_webhook.rb, this service extracts relevant attributes and relationships into a standardized format that varies per webhook family.

All payloads share this core envelope structure:

{
  "event": "submission.created",
  "account_id": 42,
  "payload": { }
}

The payload object contains serialized model data specific to the event type, with detailed schemas documented in the docs/webhooks/ directory.

3. URL Resolution and Secret Retrieval

For each event, the system queries active WebhookUrl records from app/models/webhook_url.rb. Each row stores:

  • The target HTTPS endpoint URL
  • A unique secret string for HMAC signature generation
  • Account associations for multi-tenant scoping

The job iterates over all active URLs for the relevant account, preparing individual signed requests for each endpoint.

4. HTTP Delivery and HMAC Signing

The SendWebhookRequest module in lib/send_webhook_request.rb constructs and dispatches POST requests. It cryptographically signs each payload using the stored secret:

def self.call(url:, secret:, event:, payload:, attempt:)
  body = { event: event, account_id: payload[:account_id], payload: payload }.to_json
  signature = OpenSSL::HMAC.hexdigest('SHA256', secret, body)

  response = Faraday.post(url) do |req|
    req.headers['Content-Type'] = 'application/json'
    req.headers['X-Docuseal-Signature'] = signature
    req.body = body
  end
  
  attempt.update(status: response.status, response_body: response.body)
  response.success?
rescue => e
  attempt.update(status: 0, error_message: e.message)
  false
end

This generates an X-Docuseal-Signature header containing an HMAC-SHA256 hash, ensuring payload integrity and authenticity verification on the receiver side.

5. Event Logging and Retry Logic

The system persists delivery attempts using two models defined in app/models/webhook_event.rb and app/models/webhook_attempt.rb:

  • WebhookEvent — Aggregates all delivery attempts for a single logical event occurrence
  • WebhookAttempt — Records individual HTTP round-trips, including response status codes, bodies, and error messages

When a request returns a non-2xx status or encounters a network error, the job re-queues itself with exponential backoff. Each retry creates a new WebhookAttempt record linked to the original WebhookEvent, creating a complete audit trail for debugging and compliance.

Security and Signature Verification

Each WebhookUrl stores a configurable secret used to sign payloads. Receivers must verify the X-Docuseal-Signature header to confirm the request originated from Docuseal and was not tampered with in transit:

require 'openssl'

def valid_signature?(request_body, signature_header, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, request_body)
  Rack::Utils.secure_compare(expected, signature_header)
end

Secrets can be rotated through the Webhook Secret management interface implemented in app/controllers/webhook_secret_controller.rb without interrupting webhook delivery.

Managing Webhooks via the UI

Docuseal provides administrative interfaces for webhook configuration and monitoring:

Summary

  • Docuseal provides three webhook families: Template events (created, updated, archived), Submission events (created, completed, expired, archived), and Form events (viewed, started, declined, completed)
  • Event processing follows a six-stage pipeline: detection via model callbacks, payload serialization via Submitters::SerializeForWebhook, URL resolution from WebhookUrl records, HTTP delivery with HMAC-SHA256 signing via SendWebhookRequest, persistence via WebhookEvent and WebhookAttempt models, and automatic retry with exponential backoff
  • Security relies on per-endpoint secrets stored in WebhookUrl records and transmitted via the X-Docuseal-Signature header
  • Audit trails are maintained through relational records tracking every delivery attempt, response code, and error message
  • Each event type utilizes a dedicated background job (e.g., SendSubmissionCreatedWebhookRequestJob) ensuring isolated, queue-based processing

Frequently Asked Questions

What is the retry policy for failed webhook deliveries?

Docuseal implements exponential backoff for failed webhook attempts. When SendWebhookRequest in lib/send_webhook_request.rb encounters a non-2xx response or network exception, the job re-queues itself up to a configured retry limit. Each attempt creates a new WebhookAttempt record linked to the parent WebhookEvent, providing full visibility into retry history through the database schema defined in db/migrate/20250727130628_create_webhook_events_and_attempts.rb.

How do I verify that webhook requests came from Docuseal?

Verify the X-Docuseal-Signature header against an HMAC-SHA256 hash of the raw request body using your stored webhook secret. The signature is generated in lib/send_webhook_request.rb using OpenSSL::HMAC.hexdigest('SHA256', secret, body), and must be validated using a constant-time comparison function like Rack::Utils.secure_compare to prevent timing attacks. The secret is stored per-endpoint in the webhook_urls database table.

Can I subscribe to specific webhook events rather than all events?

Yes. While each configured URL receives all events for its enabled families, the app/controllers/webhook_preferences_controller.rb interface allows account administrators to configure which webhook families (Template, Submission, Form) are active. Additionally, your receiving endpoint should filter by the event field in the JSON payload, which contains specific values like template.created or submission.completed to route processing logic accordingly.

Where are webhook configurations stored in the database?

Webhook endpoint configurations persist in the webhook_urls table (created via db/migrate/20250714172222_create_webhook_urls.rb), which stores HTTPS URLs and HMAC secrets. Delivery audit logs are maintained in the webhook_events and webhook_attempts tables (created via db/migrate/20250727130628_create_webhook_events_and_attempts.rb), capturing every request, response, and timestamp for compliance and debugging purposes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →