npm package: @railblocks/docusign-convex-component
Description: A docusign integration component for Convex.
Latest version: 0.1.6

# DocuSign Convex Component

[![npm version](https://badge.fury.io/js/@railblocks%2Fdocusign-convex-component.svg)](https://www.npmjs.com/package/@railblocks/docusign-convex-component)

A reusable Convex component for integrating DocuSign eSignature into your Convex applications.

## Features

- 🔐 **OAuth 2.0 Authentication** - Secure connection to DocuSign accounts
- 📄 **Envelope Management** - Create, send, and track envelopes
- 📋 **Template Support** - Create envelopes from templates with prefilled tabs
- 🔔 **Webhook Events** - Automatically track all envelope status changes
- 🔄 **Token Refresh** - Automatic access token refresh handling
- 💾 **Database Storage** - Stores envelopes and events in Convex tables

## Installation

```bash
npm install @railblocks/docusign-convex-component
```

## Get Started

Add the component to your `convex/convex.config.ts`:

```ts
import { defineApp } from "convex/server";
import docusignIntegration from "@railblocks/docusign-convex-component/convex.config";

const app = defineApp();
app.use(docusignIntegration);

export default app;
```

Set environment variables in your Convex dashboard:

```bash
DOCUSIGN_INTEGRATION_KEY=your_integration_key
DOCUSIGN_SECRET_KEY=your_secret_key
ENCRYPTION_KEY=your_32_byte_encryption_key_in_hex  # Generate with: openssl rand -hex 32
DOCUSIGN_ENVIRONMENT=demo  # or "production"
DOCUSIGN_HMAC_KEY=your_webhook_hmac_key  # Optional but recommended
```

Create a DocuSign instance in `convex/docusign.ts`:

```ts
import { components } from "./_generated/api";
import { DocuSign } from "@railblocks/docusign-convex-component";

export const docusign = new DocuSign(components.docusignIntegration, {});
```

Register HTTP routes in `convex/http.ts`:

```ts
import { httpRouter } from "convex/server";
import { registerDocuSignRoutes } from "@railblocks/docusign-convex-component";
import { docusign } from "./docusign";

const http = httpRouter();
registerDocuSignRoutes(http, docusign);

export default http;
```

This registers:

- `GET /docusign/oauth/callback` - OAuth callback
- `POST /docusign/webhooks` - Webhook receiver

## Quick Start

### 1. Set Up Connection

**Option A: Using Existing Tokens (Recommended for Single-User Apps)**

```ts
import { action } from "./_generated/server";
import { docusign } from "./docusign";

export const setupDocuSign = action({
  handler: async ctx => {
    return await docusign.setupWithTokens(
      ctx,
      "my-company-id",
      "YOUR_ACCESS_TOKEN",
      "YOUR_REFRESH_TOKEN",
    );
  },
});
```

**Option B: OAuth Flow (For Multi-User Apps)**

```ts
export const getAuthUrl = action({
  handler: async ctx => {
    const userId = "user_123"; // Your auth logic
    return await docusign.initiateOAuth(
      ctx,
      userId,
      "http://localhost:3000/docusign/oauth/callback",
    );
  },
});
```

### 2. Create and Send an Envelope

```ts
import { internalAction } from "./_generated/server";
import { docusign } from "./docusign";

export const sendDocument = internalAction({
  handler: async ctx => {
    return await docusign.createEnvelope(ctx, {
      userId: "user_123",
      emailSubject: "Please sign this document",
      recipients: [
        {
          email: "signer@example.com",
          name: "John Doe",
          recipientId: "1",
        },
      ],
      documents: [
        {
          documentBase64: "...", // Your base64 encoded PDF
          name: "Contract.pdf",
          fileExtension: "pdf",
          documentId: "1",
        },
      ],
      status: "sent", // or "created" to send later
    });
  },
});
```

### 3. Create from Template

```ts
export const createFromTemplate = internalAction({
  handler: async ctx => {
    return await docusign.createFromTemplate(ctx, {
      userId: "user_123",
      templateId: "your-template-id",
      emailSubject: "Please sign",
      templateRoles: [
        {
          email: "signer@example.com",
          name: "John Doe",
          roleName: "Signer",
          tabs: {
            textTabs: [
              { tabLabel: "name", value: "John Doe" },
              { tabLabel: "date", value: "2024-01-15" },
            ],
          },
        },
      ],
    });
  },
});
```

### Query Envelopes

```ts
import { internalQuery } from "./_generated/server";
import { docusign } from "./docusign";

export const getEnvelope = internalQuery({
  handler: async (ctx, args: { userId: string; envelopeId: string }) => {
    return await docusign.getEnvelope(ctx, args.userId, args.envelopeId);
  },
});

export const listEnvelopes = internalQuery({
  handler: async (ctx, args: { userId: string }) => {
    return await docusign.listEnvelopes(ctx, args.userId);
  },
});
```

## Advanced Usage

### Configure Webhooks in DocuSign

1. In DocuSign Settings → Connect → Add Configuration
2. Set webhook URL: `https://your-app.convex.site/docusign/webhooks`
3. Select events: Envelope Sent, Delivered, Completed, Declined, Voided

The component automatically stores events and updates envelope statuses.

### Send an Envelope Created with status="created"

```ts
export const sendEnvelope = internalAction({
  handler: async (ctx, args: { userId: string; envelopeId: string }) => {
    return await docusign.sendEnvelope(ctx, args.userId, args.envelopeId);
  },
});
```

### Void an Envelope

```ts
export const voidEnvelope = internalAction({
  handler: async (ctx, args: { userId: string; envelopeId: string }) => {
    return await docusign.voidEnvelope(ctx, args.userId, args.envelopeId, "No longer needed");
  },
});
```

### Custom API Calls

Need to call a DocuSign endpoint not covered by the built-in methods?

```ts
export const listTemplates = action({
  handler: async ctx => {
    return await docusign.makeAPICall(ctx, "user_123", "/templates", "GET");
  },
});
```

See [DocuSign REST API docs](https://developers.docusign.com/docs/esign-rest-api/reference/) for all
available endpoints.

### DocuSign Component Options

```ts
const docusign = new DocuSign(components.docusignIntegration, {
  integrationKey: "...", // Optional, reads from env
  secretKey: "...", // Optional, reads from env
  encryptionKey: "...", // Required for security
  environment: "production", // "demo" or "production"
  hmacKey: "...", // For webhook validation
});
```

## API Reference

### OAuth

- `docusign.initiateOAuth(ctx, userId, redirectUri, state?)` - Get authorization URL
- `docusign.setupWithTokens(ctx, userId, accessToken, refreshToken)` - Use existing tokens

### Envelopes

- `docusign.createEnvelope(ctx, options)` - Create envelope with documents
- `docusign.createEmptyEnvelope(ctx, options)` - Create empty envelope
- `docusign.createFromTemplate(ctx, options)` - Create from template
- `docusign.sendEnvelope(ctx, userId, envelopeId)` - Send an envelope
- `docusign.voidEnvelope(ctx, userId, envelopeId, reason)` - Void an envelope
- `docusign.syncEnvelopeStatus(ctx, userId, envelopeId)` - Sync status from DocuSign
- `docusign.getEnvelope(ctx, userId, envelopeId)` - Get envelope from database
- `docusign.listEnvelopes(ctx, userId, paginationOpts?)` - List user's envelopes

### Webhooks

- `docusign.handleDocuSignWebhook(ctx, req)` - Handle webhook (used internally)
- `docusign.getEnvelopeEvents(ctx, userId, envelopeId)` - Get all events for an envelope
- `docusign.getLatestEvent(ctx, userId, envelopeId)` - Get latest event

### Custom

- `docusign.makeAPICall(ctx, userId, path, method?, body?, headers?)` - Make any DocuSign API call

## Security

- **ENCRYPTION_KEY** is required - OAuth tokens are encrypted at rest
- **DOCUSIGN_HMAC_KEY** is strongly recommended - validates webhook authenticity
- Generate encryption key: `openssl rand -hex 32`

## License

MIT

## Support

[File an issue](https://github.com/railblockscom/docusign-convex-component/issues)

          {
            email: "signer@example.com",
            name: "John Doe",
            recipientId: "1",
          },
        ],
        status: "created",
      },
    );

}, });

````

### 3. Create an Envelope from Template

> **Important:** The `tabLabel` must match the **Data Label** field you set in your DocuSign template for each field. You can find this in the DocuSign template editor under the field's properties.

```ts
import { internalAction } from "./_generated/server";
import { internal } from "./_generated/api";

export const createFromTemplate = internalAction({
  args: {},
  handler: async (ctx) => {
    const userId = "user_123";

    return await ctx.runAction(
      internal.docusignIntegration.envelopes.createFromTemplate,
      {
        userId,
        templateId: "your-template-id",
        emailSubject: "Please sign this document",
        templateRoles: [
          {
            email: "signer@example.com",
            name: "John Doe",
            roleName: "Signer",  // Must match role name in template
            tabs: {
              textTabs: [
                // tabLabel must match the "Data Label" in your template
                { tabLabel: "name", value: "John Doe" },
                { tabLabel: "date", value: "2024-01-15" },
              ],
            },
          },
        ],
        status: "created",
      },
    );
  },
});
````

### 4. Send an Envelope

```ts
import { internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const sendEnvelope = internalAction({
  args: { envelopeId: v.string() },
  handler: async (ctx, args) => {
    const userId = "user_123";

    return await ctx.runAction(internal.docusignIntegration.envelopes.sendEnvelope, {
      userId,
      envelopeId: args.envelopeId,
    });
  },
});
```

### 5. Query Envelopes

```ts
import { internalQuery } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const getEnvelope = internalQuery({
  args: { envelopeId: v.string() },
  handler: async (ctx, args) => {
    return await ctx.runQuery(internal.docusignIntegration.envelopes.getEnvelope, {
      envelopeId: args.envelopeId,
    });
  },
});

export const listEnvelopes = internalQuery({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    return await ctx.runQuery(internal.docusignIntegration.envelopes.listEnvelopes, {
      userId: args.userId,
    });
  },
});
```

## Webhook Configuration

To receive webhook events from DocuSign:

1. In DocuSign Settings → Connect → Add Configuration
2. Set the webhook URL to: `https://your-app.convex.site/docusign/webhooks`
3. Select the envelope events you want to track:
   - Envelope Sent (event: `envelope-sent`, status: `sent`)
   - Envelope Delivered (event: `envelope-delivered`, status: `delivered`)
   - Envelope Completed (event: `envelope-completed`, status: `completed`)
   - Envelope Declined (event: `envelope-declined`, status: `declined`)
   - Envelope Voided (event: `envelope-voided`, status: `voided`)
   - Recipient Completed (event: `recipient-completed`)
4. (Recommended) Enable HMAC signature validation in Connect settings

**Important:** DocuSign uses different naming conventions:

- **Webhook event types** use hyphenated format (e.g., `envelope-sent`)
- **Envelope status** field uses lowercase (e.g., `sent`)
- The component automatically handles the conversion

The component will automatically store all events and update envelope statuses in your database.

## API Reference

See the complete example in [example/convex/example.ts](./example/convex/example.ts).

### Main Functions

#### OAuth (Public)

- `oauth.initiateOAuth` - Get authorization URL (public action)
- `oauth.setupWithTokens` - Set up connection with existing access/refresh tokens (public action)

#### OAuth (Internal)

- `oauth.completeOAuth` - Complete OAuth flow (called by callback)
- `oauth.getValidConnection` - Get valid connection with auto-refresh

#### Envelopes (Internal)

All envelope functions are internal for security:

- `envelopes.createEnvelopeWithDocuments` - Create envelope with PDF/document attachments
- `envelopes.createEmptyEnvelope` - Create empty envelope (for templates or manual document
  addition)
- `envelopes.createFromTemplate` - Create from template with prefilled tabs
- `envelopes.sendEnvelope` - Send an envelope
- `envelopes.voidEnvelope` - Void an envelope
- `envelopes.syncEnvelopeStatus` - Sync status from DocuSign
- `envelopes.getEnvelope` - Get envelope from database
- `envelopes.listEnvelopes` - List user's envelopes (with pagination)

#### Webhooks (Public Queries)

- `webhooks.getEnvelopeEvents` - Get all events for an envelope
- `webhooks.getLatestEvent` - Get latest event for an envelope

#### Custom API Calls

- `custom.makeAPICall` - Make any DocuSign REST API call not covered by built-in functions
- `custom.getTemplate` - Get template details including all available tabs/fields
- `custom.listTemplates` - List all templates in the account
- `custom.getTemplateTabs` - Get all tabs for a specific recipient in a template

## Discovering Template Tabs

Before using a template, you can discover what Data Labels (tab labels) are available:

```ts
import { action } from "./_generated/server";
import { docusign } from "./docusign";

export const inspectTemplate = action({
  args: { templateId: v.string() },
  handler: async (ctx, args) => {
    // Get full template with all tabs
    const template = await docusign.getTemplate(ctx, "user_123", args.templateId);

    // Or get tabs for a specific recipient
    const tabs = await docusign.getTemplateTabs(ctx, "user_123", args.templateId, "1");

    // See what Data Labels are available
    console.log(
      "Available text fields:",
      tabs.textTabs?.map(t => t.tabLabel),
    );
    console.log(
      "Available checkboxes:",
      tabs.checkboxTabs?.map(t => t.tabLabel),
    );

    return tabs;
  },
});
```

## Custom API Calls

Need to call a DocuSign endpoint not covered by the built-in functions? Use the `custom.makeAPICall`
action:

```ts
import { action } from "./_generated/server";
import { components } from "./_generated/api";

// List all templates
export const listTemplates = action({
  args: {},
  handler: async ctx => {
    const userId = "user_123"; // Your auth logic

    return await ctx.runAction(components.docusignIntegration.custom.makeAPICall, {
      userId,
      path: "/templates",
      method: "GET",
    });
  },
});

// Get envelope documents
export const getEnvelopeDocuments = action({
  args: { envelopeId: v.string() },
  handler: async (ctx, args) => {
    const userId = "user_123";

    return await ctx.runAction(components.docusignIntegration.custom.makeAPICall, {
      userId,
      path: `/envelopes/${args.envelopeId}/documents`,
      method: "GET",
    });
  },
});

// Create a brand
export const createBrand = action({
  args: { brandName: v.string() },
  handler: async (ctx, args) => {
    const userId = "user_123";

    return await ctx.runAction(components.docusignIntegration.custom.makeAPICall, {
      userId,
      path: "/brands",
      method: "POST",
      body: {
        brandName: args.brandName,
        defaultBrandLanguage: "en",
      },
    });
  },
});
```

The `makeAPICall` function:

- Automatically handles OAuth authentication and token refresh
- Supports relative paths (e.g., `/templates`) or absolute paths
- Accepts any HTTP method (GET, POST, PUT, DELETE, etc.)
- Passes through request body and headers
- Returns the raw JSON response from DocuSign

See
[DocuSign REST API documentation](https://developers.docusign.com/docs/esign-rest-api/reference/)
for all available endpoints.

## Development

Run the example app:

```bash
npm install
npm run dev
```

## License

MIT

## Support

[File an issue](https://github.com/railblockscom/docusign-convex-component/issues)