Webhook Setup
Configure webhook endpoints and event subscriptions
Webhook Setup
This guide walks you through setting up webhook endpoints to receive real-time notifications from Seal.
Prerequisites
- An HTTPS endpoint that can receive POST requests
- API key with
seal:webhooks:managescope - Ability to store and use webhook secrets securely
Step 1: Create Webhook Endpoint
Use the Webhooks API to create a new endpoint:
const response = await fetch("https://seal.convex.site/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer ak_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://api.example.com/webhooks/seal",
events: ["document.sent", "document.completed", "recipient.signed"],
description: "Production webhook endpoint",
}),
});
const webhook = await response.json();
console.log("Webhook ID:", webhook.id);
console.log("Secret:", webhook.secret); // Save this securely!Important: The webhook secret is only shown once during creation. Store it securely in your environment variables or secrets manager.
Step 2: Implement Webhook Handler
Create an HTTP endpoint that receives and processes webhook events:
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
const app = express();
// Use raw body for signature verification
app.use("/webhooks/seal", express.raw({ type: "application/json" }));
app.post("/webhooks/seal", async (req, res) => {
try {
// Step 1: Verify signature
const signature = req.headers["x-seal-signature"] as string;
const eventId = req.headers["x-seal-event-id"] as string;
const timestamp = req.headers["x-seal-timestamp"] as string;
const payload = req.body.toString("utf8");
if (!verifySignature(payload, signature, eventId, timestamp, process.env.WEBHOOK_SECRET!)) {
console.error("Invalid webhook signature");
return res.status(401).send("Unauthorized");
}
// Step 2: Parse event
const event = JSON.parse(payload);
console.log("Received event:", event.type, event.id);
// Step 3: Process event asynchronously
processEventAsync(event).catch((err) => {
console.error("Event processing failed:", err);
});
// Step 4: Respond immediately
res.status(200).send("OK");
} catch (error) {
console.error("Webhook handler error:", error);
res.status(500).send("Internal Server Error");
}
});
function verifySignature(
payload: string,
signature: string,
eventId: string,
timestamp: string,
secret: string,
): boolean {
const signedContent = `${eventId}.${timestamp}.${payload}`;
const expectedSignature = `v1=${createHmac("sha256", secret)
.update(signedContent)
.digest("hex")}`;
if (signature.length !== expectedSignature.length) {
return false;
}
return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
async function processEventAsync(event: any) {
switch (event.type) {
case "document.completed":
await handleDocumentCompleted(event.data);
break;
case "recipient.signed":
await handleRecipientSigned(event.data);
break;
default:
console.log("Unhandled event type:", event.type);
}
}
app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});Step 3: Test Your Endpoint
Local Testing with ngrok
Expose your local server for testing:
# Start your local server
npm run dev
# In another terminal, start ngrok
ngrok http 3000
# Use the HTTPS URL in your webhook configuration
# Example: https://abc123.ngrok.io/webhooks/sealSend Test Event
Trigger a test event to verify your endpoint:
const response = await fetch("https://seal.convex.site/api/v1/webhooks/wh_abc123/test", {
method: "POST",
headers: {
Authorization: "Bearer ak_your_api_key_here",
},
});Event Subscription Patterns
Subscribe to Specific Events
{
"events": ["document.completed", "recipient.signed"]
}Subscribe to All Events
Pass an empty array to receive every event type:
{
"events": []
}Managing Webhooks
List All Webhooks
const response = await fetch("https://seal.convex.site/api/v1/webhooks", {
headers: {
Authorization: "Bearer ak_your_api_key_here",
},
});
const webhooks = await response.json();Update Webhook
const response = await fetch("https://seal.convex.site/api/v1/webhooks/wh_abc123", {
method: "PATCH",
headers: {
Authorization: "Bearer ak_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
events: ["document.sent", "document.completed", "recipient.signed"],
status: "active",
}),
});Pause Webhook
Temporarily stop receiving events without deleting the endpoint:
const response = await fetch("https://seal.convex.site/api/v1/webhooks/wh_abc123", {
method: "PATCH",
headers: {
Authorization: "Bearer ak_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
status: "paused",
}),
});Delete Webhook
const response = await fetch("https://seal.convex.site/api/v1/webhooks/wh_abc123", {
method: "DELETE",
headers: {
Authorization: "Bearer ak_your_api_key_here",
},
});Webhook Status States
| Status | Description |
|---|---|
active | Webhook is receiving events |
paused | Webhook is temporarily disabled |
disabled | Webhook failed too many times and was auto-disabled |
Auto-Disable: Webhooks are automatically disabled after 10 consecutive delivery failures. Re-enable them after fixing the issue.
Slack Notifications
Instead of building a custom webhook handler, you can send event notifications directly to a Slack channel using Slack Incoming Webhooks.
Setup
- In your Slack workspace, create an Incoming Webhook
- Copy the webhook URL (starts with
https://hooks.slack.com/services/...) - In Seal, go to Settings > Developer > Webhooks and click Connect Slack
- Paste your webhook URL, name the integration, and select which events to receive
How It Works
Slack endpoints receive the same events as JSON webhooks, but Seal automatically formats them as Slack Block Kit messages with:
- Event-specific emoji and human-readable labels
- Contextual details (document title, recipient name, decline reason, etc.)
- A footer with event type and timestamp
No signature verification is needed — Slack handles authentication via the webhook URL.
Example Message
When a recipient signs a document, your Slack channel receives:
✍️ Recipient Signed
Recipient: Jane Doe
Email: jane@example.com
Seal · recipient.signed · 2026-03-12T12:00:00ZProduction Checklist
- Use HTTPS endpoint (required in production)
- Verify webhook signatures on every request
- Respond with 200 OK within 5 seconds
- Process events asynchronously
- Implement idempotency using
event.id - Store webhook secret securely (environment variables)
- Monitor webhook delivery logs
- Set up error alerting for failed deliveries
- Rotate webhook secrets every 90 days
Next Steps
- Signature Verification - Implement secure signature validation
- Webhooks API - Complete API reference
- Error Handling - Handle webhook errors
Last updated on