A docusign integration component for Convex.
npm install @railblocks/docusign-convex-component
A reusable Convex component for integrating DocuSign eSignature into your Convex applications.
- 🔐 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
``bash`
npm install @railblocks/docusign-convex-component
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 callbackPOST /docusign/webhooks
- - Webhook receiver
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",
);
},
});
`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
});
},
});
`
`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" },
],
},
},
],
});
},
});
`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);
},
});
`
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.
`ts`
export const sendEnvelope = internalAction({
handler: async (ctx, args: { userId: string; envelopeId: string }) => {
return await docusign.sendEnvelope(ctx, args.userId, args.envelopeId);
},
});
`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");
},
});
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 for all
available endpoints.
`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
});
- docusign.initiateOAuth(ctx, userId, redirectUri, state?) - Get authorization URLdocusign.setupWithTokens(ctx, userId, accessToken, refreshToken)
- - Use existing tokens
- docusign.createEnvelope(ctx, options) - Create envelope with documentsdocusign.createEmptyEnvelope(ctx, options)
- - Create empty envelopedocusign.createFromTemplate(ctx, options)
- - Create from templatedocusign.sendEnvelope(ctx, userId, envelopeId)
- - Send an envelopedocusign.voidEnvelope(ctx, userId, envelopeId, reason)
- - Void an envelopedocusign.syncEnvelopeStatus(ctx, userId, envelopeId)
- - Sync status from DocuSigndocusign.getEnvelope(ctx, userId, envelopeId)
- - Get envelope from databasedocusign.listEnvelopes(ctx, userId, paginationOpts?)
- - List user's envelopes
- docusign.handleDocuSignWebhook(ctx, req) - Handle webhook (used internally)docusign.getEnvelopeEvents(ctx, userId, envelopeId)
- - Get all events for an envelopedocusign.getLatestEvent(ctx, userId, envelopeId)
- - Get latest event
- docusign.makeAPICall(ctx, userId, path, method?, body?, headers?) - Make any DocuSign API call
- 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
MIT
{
email: "signer@example.com",
name: "John Doe",
recipientId: "1",
},
],
status: "created",
},
);
}, });
``
> 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",
},
);
},
});
``
`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,
});
},
});
`
`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,
});
},
});
`
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/webhooksenvelope-sent
3. Select the envelope events you want to track:
- Envelope Sent (event: , status: sent)envelope-delivered
- Envelope Delivered (event: , status: delivered)envelope-completed
- Envelope Completed (event: , status: completed)envelope-declined
- Envelope Declined (event: , status: declined)envelope-voided
- Envelope Voided (event: , status: voided)recipient-completed
- Recipient Completed (event: )
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)sent
- Envelope status field uses lowercase (e.g., )
- The component automatically handles the conversion
The component will automatically store all events and update envelope statuses in your database.
See the complete example in example/convex/example.ts.
#### 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 attachmentsenvelopes.createEmptyEnvelope
- - Create empty envelope (for templates or manual documentenvelopes.createFromTemplate
addition)
- - Create from template with prefilled tabsenvelopes.sendEnvelope
- - Send an envelopeenvelopes.voidEnvelope
- - Void an envelopeenvelopes.syncEnvelopeStatus
- - Sync status from DocuSignenvelopes.getEnvelope
- - Get envelope from databaseenvelopes.listEnvelopes
- - List user's envelopes (with pagination)
#### Webhooks (Public Queries)
- webhooks.getEnvelopeEvents - Get all events for an envelopewebhooks.getLatestEvent
- - Get latest event for an envelope
#### Custom API Calls
- custom.makeAPICall - Make any DocuSign REST API call not covered by built-in functionscustom.getTemplate
- - Get template details including all available tabs/fieldscustom.listTemplates
- - List all templates in the accountcustom.getTemplateTabs
- - Get all tabs for a specific recipient in a template
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;
},
});
`
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
for all available endpoints.
Run the example app:
`bash``
npm install
npm run dev
MIT