---
title: Webhook Setup
description: Configure webhooks to receive real-time notifications about document events.
---

import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

## Overview

Webhooks are HTTP callbacks triggered by specific events in Documenso. When an event occurs (such as a document being signed), Documenso sends an HTTP POST request to your configured URL with details about the event.

Common use cases include:

- Syncing document status with your database
- Triggering automated workflows when documents are signed
- Integrating with CRM systems or other third-party services
- Sending custom notifications to stakeholders

<Callout type="info">
  Webhooks are available for teams only. Personal accounts cannot configure webhooks.
</Callout>

## Creating a Webhook Endpoint

Before configuring a webhook in Documenso, you need an endpoint that can receive HTTP POST requests. Here's a minimal example:

<Tabs items={['Node.js (Express)', 'Python (Flask)', 'Go']}>
<Tab value="Node.js (Express)">
```javascript
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/documenso', (req, res) => {
  const { event, payload, createdAt } = req.body;

  console.log(`Received event: ${event}`);
  console.log(`Document ID: ${payload.id}`);
  console.log(`Document title: ${payload.title}`);

  // Process the webhook event
  switch (event) {
    case 'DOCUMENT_COMPLETED':
      // Handle completed document
      break;
    case 'DOCUMENT_SIGNED':
      // Handle signed document
      break;
    // Handle other events...
  }

  // Respond with 200 OK to acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

````
</Tab>
<Tab value="Python (Flask)">
```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/documenso', methods=['POST'])
def handle_webhook():
    data = request.get_json()

    event = data.get('event')
    payload = data.get('payload')

    print(f"Received event: {event}")
    print(f"Document ID: {payload.get('id')}")
    print(f"Document title: {payload.get('title')}")

    # Process the webhook event
    if event == 'DOCUMENT_COMPLETED':
        # Handle completed document
        pass
    elif event == 'DOCUMENT_SIGNED':
        # Handle signed document
        pass

    # Respond with 200 OK to acknowledge receipt
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)
````

</Tab>
<Tab value="Go">
```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type WebhookPayload struct {
	Event     string                 `json:"event"`
	Payload   map[string]interface{} `json:"payload"`
	CreatedAt string                 `json:"createdAt"`
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	var data WebhookPayload
	if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
		http.Error(w, "Invalid payload", http.StatusBadRequest)
		return
	}

	fmt.Printf("Received event: %s\n", data.Event)
	fmt.Printf("Document ID: %v\n", data.Payload["id"])
	fmt.Printf("Document title: %v\n", data.Payload["title"])

	// Process the webhook event
	switch data.Event {
	case "DOCUMENT_COMPLETED":
		// Handle completed document
	case "DOCUMENT_SIGNED":
		// Handle signed document
	}

	// Respond with 200 OK to acknowledge receipt
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
	http.HandleFunc("/webhooks/documenso", webhookHandler)
	fmt.Println("Webhook server running on port 3000")
	http.ListenAndServe(":3000", nil)
}

```
</Tab>
</Tabs>

<Callout type="warn">
  Always respond with a `2xx` status within 10 seconds. Documenso will retry failed deliveries according to the configured background-job provider.
</Callout>

## Configuring Webhooks in Documenso via the Dashboard

{/* prettier-ignore */}
<Steps>
<Step>
### Navigate to team settings

Click your avatar in the top right corner and select **Team settings** from the dropdown menu.
</Step>

<Step>
### Open the webhooks tab

Navigate to the **Webhooks** tab in the team settings sidebar.

![Webhooks settings page](/webhook-images/webhooks-page.webp)
</Step>

<Step>
### Create a new webhook

Click the **Create Webhook** button to open the configuration dialog.

![Create webhook dialog](/webhook-images/create-webhook-dialog.webp)
</Step>

<Step>
### Configure the webhook

Fill in the following fields:

| Field | Description |
| ----- | ----------- |
| **Webhook URL** | The HTTP or HTTPS endpoint that will receive webhook events |
| **Events** | Select which events should trigger this webhook |
| **Secret** (optional) | A secret key used to sign the payload for verification |
</Step>

<Step>
### Save the webhook

Click **Create Webhook** to save your configuration. The webhook is now active and will receive events.
</Step>
</Steps>

## Webhook URL Requirements

Your webhook endpoint must meet these requirements:

| Requirement | Details |
| ----------- | ------- |
| **Protocol** | HTTP and HTTPS are accepted; use HTTPS in production |
| **Response** | Must return a `2xx` status code within 10 seconds |
| **Method** | Must accept HTTP POST requests |
| **Content-Type** | Must accept `application/json` payloads |
| **Availability** | Must be publicly accessible from the internet |

<Callout type="warn">
  Documenso performs a best-effort check that rejects webhook URLs which use or resolve to private
  or loopback addresses. This is not a complete SSRF mitigation — it does not cover DNS rebinding
  and fails open on DNS lookup errors or timeouts — so self-hosted deployments should still enforce
  network-level egress rules. Self-hosters that need to deliver to a hostname resolving to a
  private address can add that hostname to the comma-separated
  `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` environment variable.
</Callout>

<Callout type="info">
  For local development, use a tunneling service like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.me) to expose your local server.
</Callout>

## Selecting Events

When creating a webhook, you can subscribe to one or more events:

| Event | Trigger |
| ----- | ------- |
| `DOCUMENT_CREATED` | A new document is created |
| `DOCUMENT_SENT` | A document is sent to recipients |
| `DOCUMENT_OPENED` | A recipient opens the document for the first time |
| `DOCUMENT_SIGNED` | A recipient signs the document |
| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action |
| `DOCUMENT_COMPLETED` | All recipients have completed their actions |
| `DOCUMENT_REJECTED` | A recipient rejects the document |
| `DOCUMENT_CANCELLED` | A pending document is explicitly cancelled or a document owner deletes it |
| `RECIPIENT_EXPIRED` | A recipient's signing deadline passes before they sign or reject |
| `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient |
| `TEMPLATE_CREATED` | A new template is created |
| `TEMPLATE_UPDATED` | A template is modified |
| `TEMPLATE_DELETED` | A template is deleted |
| `TEMPLATE_USED` | A document is created from a template |

You can subscribe to all events or select specific ones based on your needs. For example, if you only need to know when documents are fully signed, subscribe only to `DOCUMENT_COMPLETED`.

See [Webhook Events](/docs/developers/webhooks/events) for detailed payload information for each event type.

## Testing Webhooks

Documenso provides a built-in testing feature to verify your webhook endpoint works correctly.

{/* prettier-ignore */}
<Steps>
<Step>
### Navigate to webhook details

Go to **Team Settings > Webhooks** and click on the webhook you want to test.

![Webhook detail page](/webhook-images/webhook-detail-page.webp)
</Step>

<Step>
### Click test

Click the **Test** button in the webhook details page.
</Step>

<Step>
### Select an event type

Choose which event type you want to simulate from the dropdown.
</Step>

<Step>
### Send test payload

Click **Send** to dispatch a test webhook with sample data to your endpoint.

![Webhook test trigger](/webhook-images/webhook-test-trigger.webp)
</Step>
</Steps>

The test payload contains realistic sample data so you can verify your endpoint processes events correctly. After sending, you can view the response in the webhook call logs.

### Viewing Webhook Logs

Each webhook subscription maintains a log of all delivery attempts. To view logs:

{/* prettier-ignore */}
<Steps>
<Step>
Go to **Team Settings > Webhooks**
</Step>
<Step>
Click on a webhook to view its details
</Step>
<Step>
Review the logs

Each webhook call shows the following details:
- Status (success/failure)
- Event type
- Timestamp
- Response code
- Request and response bodies
- Response headers

Click any call to see full details including headers and response data.
</Step>
</Steps>

### Resending Failed Webhooks

If a webhook delivery fails, you can manually resend it:

{/* prettier-ignore */}
<Steps>
<Step>
Navigate to the webhook call details page
</Step>
<Step>
Click the **Resend** button
</Step>
<Step>
Documenso will attempt to deliver the same payload again
</Step>
</Steps>

## Retry Policy

A delivery fails when the endpoint returns a non-`2xx` response, the 10-second timeout expires, or the request fails. Redirects are not followed, so `3xx` responses also fail. Network and SSRF-blocked requests are recorded with response code `0`.

For self-hosted deployments, retries are handled by the background-job provider selected with `NEXT_PRIVATE_JOBS_PROVIDER`:

| Provider | Total attempts | Retry timing |
| -------- | -------------- | ------------ |
| Local (default) | 4 | Back-to-back, with no backoff |
| BullMQ | 3 | Exponential backoff starting at 1 second |
| Inngest | 5 | Inngest platform backoff |

Only the individual delivery (`WebhookCall`) record is marked as failed. Documenso does not automatically disable the webhook or apply a circuit breaker, so future matching events continue to be delivered. After automatic attempts are exhausted, you can manually resend a failed delivery from the dashboard.

<Callout type="warn">
  If your endpoint consistently fails, consider reviewing your server logs and ensuring your endpoint meets all [URL requirements](#webhook-url-requirements).
</Callout>

## Security Best Practices

<Accordions type="multiple">
  <Accordion title="Use HTTPS">
    Always use HTTPS endpoints in production.
  </Accordion>
  <Accordion title="Verify signatures">
    Use a webhook secret and verify the `X-Documenso-Secret` header (see [Verification](/docs/developers/webhooks/verification)).
  </Accordion>
  <Accordion title="Validate payloads">
    Validate incoming data before processing.
  </Accordion>
  <Accordion title="Respond quickly">
    Return 200 OK immediately, then process asynchronously.
  </Accordion>
  <Accordion title="Idempotency">
    Handle duplicate deliveries gracefully.
  </Accordion>
</Accordions>

## Next Steps

- [Webhook Events](/docs/developers/webhooks/events) - Detailed payload structure for each event type
- [Webhook Verification](/docs/developers/webhooks/verification) - Secure your webhooks with signature verification
```
