---
title: Webhook Setup
description: 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:

```typescript
const response = await fetch("https://api.seal.nyc/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!
```

> **Warn**
>
> **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:

```typescript
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:

```bash
# 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:

```typescript
const response = await fetch(
  "https://api.seal.nyc/api/v1/webhooks/wh_abc123/test",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer ak_your_api_key_here",
    },
  }
);
```

## Event Subscription Patterns

### Subscribe to Specific Events

```json
{
  "events": ["document.completed", "recipient.signed"]
}
```

### Subscribe to All Events

Pass an empty array to receive every event type:

```json
{
  "events": []
}
```

## Managing Webhooks

### List All Webhooks

```typescript
const response = await fetch("https://api.seal.nyc/api/v1/webhooks", {
  headers: {
    Authorization: "Bearer ak_your_api_key_here",
  },
});

const webhooks = await response.json();
```

### Update Webhook

```typescript
const response = await fetch("https://api.seal.nyc/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:

```typescript
const response = await fetch("https://api.seal.nyc/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

```typescript
const response = await fetch("https://api.seal.nyc/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 |

> **Info**
>
> **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](https://api.slack.com/messaging/webhooks)
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](https://api.slack.com/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

- **[Signature Verification](/docs/webhooks/verification)** - Implement secure signature validation
- **[Webhooks API](/docs/api-reference/webhooks)** - Complete API reference
- **[Error Handling](/docs/getting-started/error-handling)** - Handle webhook errors
