Seal Docs

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:manage scope
  • 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/seal

Send 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

StatusDescription
activeWebhook is receiving events
pausedWebhook is temporarily disabled
disabledWebhook 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

  1. In your Slack workspace, create an Incoming Webhook
  2. Copy the webhook URL (starts with https://hooks.slack.com/services/...)
  3. In Seal, go to Settings > Developer > Webhooks and click Connect Slack
  4. 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:00Z

Production 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

Last updated on

On this page